Repository navigation
Cache: run the cache in a dedicated handler function - #43
Conversation
…y into core adapter feat: enhance build process with additional server bundle customization options chore: update package.json scripts for improved build and testing workflow test: add unit tests for adapter build process and server bundle generation fix: ensure proper handling of external dependencies and edge configuration in server bundle
…ests for resolve plugin
…specific overrides
…udflare specific overrides" This reverts commit 37c4d90.
…ad of throwing - Extract ValidateConfigResult type with success/message/shouldThrow/level - Convert validateFunctionOptions and validateSplittedFunctionOptions to return result objects - Remove logger dependency from validateConfig.ts - Preserve compatibilityMatrix, TODO comment, @ts-expect-error pragmas - Add 5 characterization tests in validateConfig.spec.ts - No caller impact: compileConfig.ts is the sole importer (updated in T3)
- Export OpenNextOutput interface (was internal) - Extract buildOpenNextOutput(buildOpts) for construction-only (no fs write) - Keep legacy generateOutput as thin wrapper (construction + file write) - Preserve all construction logic verbatim, including @ts-expect-error - Add 3 characterization tests in generateOutput.spec.ts - Backward compatible: byte-equivalent output to today
- Replace bare validateConfig(config) call with result-handling block - Throw on shouldThrow:true (bad routes — preserves existing behavior) - Log at appropriate level on shouldThrow:false (level field from T1) - All 3 export signatures and edge-runtime detection block unchanged - Direct callers (aws/build.ts, cloudflare/utils.ts) unaffected
…OpenNextAdapterOptions - Make OpenNextAdapterOptions<T = OpenNextOutput> and buildAdapter<T> generic - Add validateConfig override hook (runs after callback in modifyConfig) - Add generateOutput override hook (returns T, gated by skipGenerateOutput) - buildAdapter serializes override return via fs.writeFileSync (override never touches fs) - Default path uses buildOpenNextOutput (extracted in T2) - Add 5 new tests covering override behaviors + default path + skipGenerateOutput - All 16 existing adapter tests preserved; AWS/Cloudflare adapters compile with default T
When an adapter config specifies full package-specifier paths (e.g., @opennextjs/aws/overrides/wrappers/aws-lambda.js), esbuild cannot resolve them during bundling. Use createRequire(args.path).resolve() in the openNextResolvePlugin to convert package specifiers to filesystem-relative paths at build time, falling back to the original value if resolution fails. This fixes the openbuild:local build error: ERROR: Could not resolve "@opennextjs/aws/overrides/wrappers/aws-lambda.js" ERROR: Could not resolve "@opennextjs/aws/overrides/tagCache/dynamodb.js" Added test I verifying resolution of a mock package in node_modules.
…solution and improve path handling
commit: |
| cacheFunction: config.dangerous?.disableIncrementalCache | ||
| ? undefined | ||
| : { | ||
| handler: indexHandler, | ||
| bundle: ".open-next/cache-function", | ||
| }, |
There was a problem hiding this comment.
🟡 Build manifest advertises a cache bundle that was never produced
The generated build output always lists the cache function bundle (cacheFunction at packages/core/src/build/generateOutput.ts:349-354) even when the adapter asked to skip building it, so deployment tooling points at a directory that does not exist.
Impact: Adapters that opt out of the cache function get a build manifest referencing a missing bundle, which can break deployment.
skipCache is not visible to generateOutput
buildAdapter skips the bundle when adapterOptions.skipCache is set (packages/core/src/build/adapter.ts:251-254), but buildOpenNextOutput only checks config.dangerous?.disableIncrementalCache. An adapter using skipCache: true together with the default generateOutput therefore emits { handler, bundle: ".open-next/cache-function" } for a bundle that was never created.
Prompt for agents
packages/core/src/build/adapter.ts skips createCacheBundle when adapterOptions.skipCache is true, but packages/core/src/build/generateOutput.ts (buildOpenNextOutput) only omits the `cacheFunction` entry based on config.dangerous?.disableIncrementalCache. When an adapter sets skipCache and relies on the default output generation, the manifest advertises .open-next/cache-function which does not exist. Thread the skip flag (or a build option) through to output generation so the entry is omitted consistently.
Was this helpful? React with 👍 or 👎 to provide feedback.
| if (method === "POST" && rawPath === "/cache/revalidate-tags") { | ||
| return await handleRevalidateTags(body); | ||
| } | ||
|
|
||
| // All other operations must be on /cache/* | ||
| if (!rawPath.startsWith("/cache/")) { | ||
| return buildErrorResponse("Not Found", 404); | ||
| } | ||
|
|
||
| const key = decodeURIComponent(rawPath.slice("/cache/".length)); | ||
|
|
||
| if (!key) { | ||
| return buildErrorResponse("Missing cache key", 400); | ||
| } | ||
|
|
||
| const cacheType: CacheEntryType = query?.type === "fetch" ? "fetch" : "cache"; | ||
|
|
||
| switch (method) { | ||
| case "GET": | ||
| return await handleGet(key, cacheType); | ||
| case "PUT": | ||
| return await handleSet(key, cacheType, body); | ||
| case "DELETE": | ||
| return await handleDelete(key); | ||
| default: | ||
| return buildErrorResponse("Method Not Allowed", 405); | ||
| } |
There was a problem hiding this comment.
🟨 New cache HTTP endpoints expose unauthenticated read/write/invalidate access to the cache store
The new cache handler serves GET/PUT/DELETE /cache/* and POST /cache/revalidate-tags (packages/core/src/adapters/cache-adapter.ts:74-100) with no authentication or authorization check. When deployed with an HTTP-reachable wrapper (as the fetch cache client at packages/core/src/overrides/cache/fetch.ts assumes), anyone able to reach the function URL can read arbitrary cached pages/fetch responses, overwrite them (cache poisoning of HTML/RSC served to users), delete them, or mass-invalidate tags.
Was this helpful? React with 👍 or 👎 to provide feedback.
Add a standalone cache handler function that owns the incremental cache, the tag cache and CDN invalidation, and a `cache` override that other functions use to reach it. - `adapters/cache-adapter.ts` exposes `GET/PUT/DELETE /cache/*` and `POST /cache/revalidate-tags` through `createGenericHandler`. - `build/createCacheBundle.ts` bundles it to `.open-next/cache-function`, wired into `buildAdapter` behind the new `skipCache` option and reported as `cacheFunction` in the OpenNext output. - New top level `cacheHandler` config option carries the incremental cache, tag cache and cdn invalidation overrides for that function. - New `cache` override with `fetch`, `local` and `dummy` implementations, and a shared wire format in `utils/cache-get.ts` that splits the entry metadata into headers and the payload into the body. Nothing is removed here: the existing in-process `incrementalCache` and `tagCache` overrides keep working unchanged.
467f312 to
c200d7a
Compare
Consume cache PUT and tag-revalidation payloads through the shared ReadableStream helpers, and emit streamed request bodies from the local cache transport. Handle bodyless cache responses as empty payloads instead of passing undefined to the stream reader. Preserve the cache bundle, output metadata, and resolver integration while reconciling PR #43 with the latest adapter APIs. Restore the cache bundle mock and the three-argument output assertion in adapter tests. Tests: pnpm --filter @opennextjs/core ts:check; focused core build tests (38); focused cache adapter and transport tests (53).
Use the configured cache override for incremental, fetch, composable, and routing cache storage operations. Delegate tag revalidation to that transport so the dedicated handler receives invalidation requests. Keep the transport optional and retain the existing incremental and tag cache behavior when no cache override is configured. This preserves compatibility for applications that have not opted into the dedicated cache function. Tests cover transport reads, writes, deletes, tag revalidation, composable entries, routing interception, and legacy fallback. Verified with the core type-check and 105 focused tests.
Provide cache-bundle defaults that use the AWS Lambda wrapper and API Gateway v2 converter instead of the core Node HTTP server. Back the function with the same S3 incremental cache and DynamoDB tag cache used by AWS server functions. Add adapter-level coverage for forwarding cache bundle defaults and an AWS-specific test that locks down the complete cache function profile. Tests: @opennextjs/aws type-check; 25 core adapter tests; AWS adapter cache-default test.
Split the internal cache request handler from the platform-wrapped deployment adapter. Bundle index.mjs for provider invocation and handler.mjs for direct in-process calls. Point the local cache transport at the raw handler so importing it cannot start the Node HTTP wrapper or pass InternalEvent objects through a platform converter. Keep cache initialization shared by both entrypoints. Tests assert both bundle outputs and exercise the raw handler and local transport. Verified with the core type-check, 26 build tests, and 17 cache handler tests.
Apply default-function incremental cache, tag cache, and CDN invalidation overrides when the dedicated cacheHandler does not specify replacements. This keeps existing configurations working when they opt into the new cache transport. Maintain explicit precedence: cacheHandler settings override legacy default-function providers, which in turn override adapter and core defaults. Wrapper and converter settings remain scoped to the cache handler itself. Tests cover both legacy fallback and cacheHandler precedence. Verified with the core type-check and the cache bundle test suite.
Validate cache handler responses for set, delete, and tag-revalidation operations in both fetch and local transports. Reject non-2xx results with operation-specific errors instead of reporting failed mutations as successful. Make fetch tests install a fresh response mock for every test and restore the global afterward, removing their previous ordering dependency. Tests cover successful requests and 500 responses for every mutation. Verified with the core type-check and 35 transport tests.
Parse the literal false revalidation header as a boolean rather than feeding it through numeric conversion. Keep invalid values undefined and preserve existing numeric interval handling. Apply the parser to both fetch and cached-file responses so permanent entries retain their semantics across local and HTTP cache transports. Tests cover false values for both response types. Verified with the core type-check and 59 parser and transport tests.
Move the production imports ahead of Vitest mock declarations in the two new integration tests. Vitest continues to hoist the mocks while the files now satisfy the repository import ordering rule. Tests: repository lint; focused cache handler and AWS adapter tests.
Reuse an existing OpenNext AsyncLocalStorage instance when the raw cache handler is imported in-process. This prevents the local cache transport from replacing an active request context and losing pending work or tag-write state. Add a regression test that resets the module graph, imports the raw handler with pre-existing request storage, and verifies the same instance remains installed. Tests: core type-check; repository lint; focused cache adapter tests.
There was a problem hiding this comment.
🟡 Changes recommended
One or more issues must be addressed before approval.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds an optional dedicated cache handler function and transport overrides, while preserving direct-cache behavior by default.
Changes:
- Added cache handler routes and shared serialization.
- Added fetch, local, and dummy cache transports.
- Wired cache bundling, configuration, output metadata, AWS defaults, and tests.
File summaries
| File | Description |
|---|---|
| packages/tests-unit/tests/utils/cache-get.test.ts | Updated as part of this pull request. |
| packages/tests-unit/tests/overrides/cache/local.test.ts | Updated as part of this pull request. |
| packages/tests-unit/tests/overrides/cache/fetch.test.ts | Updated as part of this pull request. |
| packages/tests-unit/tests/core/routing/cacheInterceptor.test.ts | Updated as part of this pull request. |
| packages/tests-unit/tests/build/aws-adapter.test.ts | Updated as part of this pull request. |
| packages/tests-unit/tests/adapters/composable-cache.test.ts | Updated as part of this pull request. |
| packages/tests-unit/tests/adapters/cache.test.ts | Updated as part of this pull request. |
| packages/tests-unit/tests/adapters/cache-adapter.test.ts | Updated as part of this pull request. |
| packages/core/src/utils/cache-get.ts | Updated as part of this pull request. |
| packages/core/src/types/overrides.ts | Updated as part of this pull request. |
| packages/core/src/types/open-next.ts | Updated as part of this pull request. |
| packages/core/src/types/global.ts | Updated as part of this pull request. |
| packages/core/src/plugins/resolve.ts | Updated as part of this pull request. |
| packages/core/src/overrides/cache/local.ts | Updated as part of this pull request. |
| packages/core/src/overrides/cache/fetch.ts | Updated as part of this pull request. |
| packages/core/src/overrides/cache/dummy.ts | Updated as part of this pull request. |
| packages/core/src/core/routing/cacheInterceptor.ts | Updated as part of this pull request. |
| packages/core/src/core/resolve.ts | Updated as part of this pull request. |
| packages/core/src/core/createMainHandler.ts | Updated as part of this pull request. |
| packages/core/src/core/createGenericHandler.ts | Updated as part of this pull request. |
| packages/core/src/build/generateOutput.ts | Updated as part of this pull request. |
| packages/core/src/build/createCacheBundle.ts | Updated as part of this pull request. |
| packages/core/src/build/createCacheBundle.spec.ts | Updated as part of this pull request. |
| packages/core/src/build/adapter.ts | Updated as part of this pull request. |
| packages/core/src/build/adapter.spec.ts | Updated as part of this pull request. |
| packages/core/src/adapters/middleware.ts | Updated as part of this pull request. |
| packages/core/src/adapters/composable-cache.ts | Updated as part of this pull request. |
| packages/core/src/adapters/cache.ts | Updated as part of this pull request. |
| packages/core/src/adapters/cache-handler.ts | Updated as part of this pull request. |
| packages/core/src/adapters/cache-adapter.ts | Updated as part of this pull request. |
| packages/aws/src/adapter.ts | Updated as part of this pull request. |
| .changeset/cache-handler-function.md | Updated as part of this pull request. |
Review details
Suppressed comments (5)
packages/core/src/adapters/cache-handler.ts:170
- This truthiness check accepts malformed entries such as
{ "value": "not-a-cache-entry" }and persists them. A later GET then evaluates"kind" in valueinbuildCacheGetResponse, which throws for the primitive and turns the key into a repeated 500; validate thatvalueis a non-null object (and not an array) before passing it to the incremental cache.
if (!payload.value) {
return buildErrorResponse("Missing 'value' in request body", 400);
packages/core/src/adapters/cache-handler.ts:137
- The built-in incremental caches signal a normal miss by throwing (
s3.getpropagates a missing-object error andfs-dev.getpropagatesENOENT), so ordinary cache misses enter this catch and are returned as 500 responses. The fetch/local clients currently mask that by ignoring the status, but the cache endpoint still logs every miss as an internal error and remote callers see the wrong status. Normalize not-found errors to the existing 404 response withx-opennext-cache-found: false(or make the providers returnnull) instead of treating them as handler failures.
} catch (e) {
error("Failed to get cache entry", e);
return buildErrorResponse("Failed to get cache entry", 500);
}
packages/core/src/adapters/cache.ts:310
- For the HTTP/fetch transport, this only moves cache I/O: cache reads and tag association writes still use the main function's
globalThis.tagCache, while this branch revalidatescacheHandler.tagCacheinside the separate function. With the documented configuration (for example,cache: "fetch"plus onlycacheHandler.tagCache), entries are indexed in one store and invalidated in another, so revalidated entries can continue to be served. Tag checks/association writes need to be routed through the dedicated handler as well, or the configuration must explicitly require the same tag backend in both functions.
if (globalThis.cache) {
await globalThis.cache.revalidateTags(_tags);
return;
packages/core/src/build/createCacheBundle.ts:64
- Because
cacheHandlerextendsDefaultFunctionOptions,cacheHandler.installis a supported configuration, but this bundle never callsinstallDependencies(unlike the warmer and revalidation bundles). A custom cache override that needs an installed dependency will therefore build successfully and fail when.open-next/cache-functionloads it. Installconfig.cacheHandler?.installafter esbuild.
);
packages/core/src/types/open-next.ts:484
cacheHandlerexposes the same wrapper/converter override surface as the other function configs, butvalidateConfignever validates it. Consequently incompatible values such as an AWS wrapper with the node converter bypass the existing compatibility diagnostics and can produce a broken cache function. AddvalidateFunctionOptions(config.cacheHandler ?? {})to the validation results.
cacheHandler?: DefaultFunctionOptions & {
/**
- Files reviewed: 32/32 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Reject revalidation payloads containing non-string tag values before they reach tag cache providers. Add coverage confirming malformed arrays return a 400 response without triggering tag or CDN operations.
Forward configured cache transports and adapter cache defaults to the resolve plugin for Node and edge middleware bundles. Add coverage proving the Node middleware builder includes both override sources.
This reverts commit f22f401.
Adds a dedicated cache handler function and the
cacheoverride that reaches it.adapters/cache-adapter.tsexposesGET/PUT/DELETE /cache/*andPOST /cache/revalidate-tagsthroughcreateGenericHandler, backed by theIncrementalCache,TagCacheandCDNInvalidationHandleroverrides.build/createCacheBundle.tsbundles it to.open-next/cache-function, wired intobuildAdapterbehind a newskipCacheoption and reported ascacheFunctionin the OpenNext output.cacheHandlerconfig option carries the overrides for that function.cacheimplementations:fetch,localanddummy, sharing the wire format inutils/cache-get.ts— entry metadata in headers, payload in the body.Purely additive. Nothing is removed and no behaviour changes:
default.override.incrementalCacheanddefault.override.tagCachekeep working exactly as before. The switchover happens in PR 3 of the stack.