From 23e2a48742c2469fbcb1015020c82e3e7a0f3413 Mon Sep 17 00:00:00 2001 From: kmaclip Date: Fri, 27 Mar 2026 21:13:29 -0400 Subject: [PATCH 1/2] fix(cache): sanitize cache key segments to prevent ENOTDIR on fs drivers When using `fs` or `fs-lite` as the storage driver, caching a route like `/foo` creates a file on disk. Caching `/foo/bar` then needs `/foo` to be a directory, which crashes with ENOTDIR. The root cause is that cache group and name values contain "/" characters (e.g., group "nitro/route-rules", name "/**:/foo") which unstorage's filesystem drivers interpret as directory separators after converting the ":" key separators to "/". Changes: - Replace "/" with "_" in all cache group names (nitro/functions -> nitro-functions, etc.) to prevent unexpected directory nesting - Sanitize the route-rules cache name by replacing "/" with "_" in route path keys before passing to ocache - Add sanitizeCacheKey() helper applied to user-provided cache names in defineCachedFunction/defineCachedHandler public APIs Fixes #4142 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/runtime/internal/cache.ts | 16 ++++++++++++++-- src/runtime/internal/route-rules.ts | 9 +++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/runtime/internal/cache.ts b/src/runtime/internal/cache.ts index 76aad1ce83..2da2076247 100644 --- a/src/runtime/internal/cache.ts +++ b/src/runtime/internal/cache.ts @@ -13,6 +13,16 @@ import type { CacheOptions, CachedEventHandlerOptions } from "nitro/types"; let _storageReady = false; +/** + * Sanitize a cache group or name so it is safe for filesystem-based storage + * drivers (fs, fs-lite). Unstorage maps ":" to "/" on disk, so any "/" already + * present in the value would create unexpected directory nesting and cause + * ENOTDIR errors when a path is both a file and a prefix of another path. + */ +function sanitizeCacheKey(value: string | undefined): string | undefined { + return value?.replace(/\//g, "_"); +} + function ensureStorage() { if (_storageReady) { return; @@ -37,9 +47,10 @@ export function defineCachedFunction( ): (...args: ArgsT) => Promise { ensureStorage(); return _defineCachedFunction(fn, { - group: "nitro/functions", + group: "nitro-functions", onError: defaultOnError, ...opts, + name: sanitizeCacheKey(opts.name), }); } @@ -49,12 +60,13 @@ export function defineCachedHandler( ): EventHandler { ensureStorage(); const ocacheHandler = _defineCachedHandler(handler as any, { - group: "nitro/handlers", + group: "nitro-handlers", onError: defaultOnError, toResponse: (value, event) => toResponse(value, event as H3Event), createResponse: (body, init) => new FastResponse(body, init), handleCacheHeaders: (event, conditions) => handleCacheHeaders(event as H3Event, conditions), ...opts, + name: sanitizeCacheKey(opts.name), }); return defineHandler((event) => ocacheHandler(event as any)); } diff --git a/src/runtime/internal/route-rules.ts b/src/runtime/internal/route-rules.ts index 1100bc50a6..482e76b657 100644 --- a/src/runtime/internal/route-rules.ts +++ b/src/runtime/internal/route-rules.ts @@ -70,9 +70,14 @@ export const cache: RouteRuleCtor<"cache"> = ((m) => const key = `${m.route}:${route}`; let cachedHandler = cachedHandlers.get(key); if (!cachedHandler) { + // Sanitize the name to avoid filesystem path conflicts. + // Route paths contain "/" which unstorage's fs drivers interpret as + // directory separators, causing ENOTDIR when a path like "/foo" is + // cached as a file but "/foo/bar" needs it to be a directory. + const safeName = key.replace(/\//g, "_"); cachedHandler = defineCachedHandler(handler, { - group: "nitro/route-rules", - name: key, + group: "nitro-route-rules", + name: safeName, ...m.options, }); cachedHandlers.set(key, cachedHandler); From db253b3f4dc11902ca79774e17687b60611c618d Mon Sep 17 00:00:00 2001 From: kmaclip Date: Sun, 29 Mar 2026 10:26:12 -0400 Subject: [PATCH 2/2] fix: sanitize user-provided cache group values Address CodeRabbit review: opts.group was passed through raw, so a custom group containing "/" could still trigger ENOTDIR on fs drivers. Now both group and name are sanitized through sanitizeCacheKey(). --- src/runtime/internal/cache.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/runtime/internal/cache.ts b/src/runtime/internal/cache.ts index 2da2076247..ad6a7e1074 100644 --- a/src/runtime/internal/cache.ts +++ b/src/runtime/internal/cache.ts @@ -47,9 +47,9 @@ export function defineCachedFunction( ): (...args: ArgsT) => Promise { ensureStorage(); return _defineCachedFunction(fn, { - group: "nitro-functions", onError: defaultOnError, ...opts, + group: sanitizeCacheKey(opts.group || "nitro-functions"), name: sanitizeCacheKey(opts.name), }); } @@ -60,12 +60,12 @@ export function defineCachedHandler( ): EventHandler { ensureStorage(); const ocacheHandler = _defineCachedHandler(handler as any, { - group: "nitro-handlers", onError: defaultOnError, toResponse: (value, event) => toResponse(value, event as H3Event), createResponse: (body, init) => new FastResponse(body, init), handleCacheHeaders: (event, conditions) => handleCacheHeaders(event as H3Event, conditions), ...opts, + group: sanitizeCacheKey(opts.group || "nitro-handlers"), name: sanitizeCacheKey(opts.name), }); return defineHandler((event) => ocacheHandler(event as any));