Skip to content

Cache: run the cache in a dedicated handler function - #43

Merged
vicb merged 30 commits into
mainfrom
conico/cache-1-core-fn
Sep 21, 2026
Merged

vicb merged 30 commits into
mainfrom
conico/cache-1-core-fn

Conversation

@conico974

@conico974 conico974 commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor

Part 1 of 6 of the cache stack, split out of #31. Review only this PR's diff — it is based on the previous branch.

PR Base
👉 #43 — core: dedicated cache handler function conico/share-build (#35)
#44 — cloudflare: OpenNextCache entrypoint #43
#45 — core: route all caching through the cache override ⚠️ breaking #44
#46 — port SWR tag revalidation from AWS #45
#47 — core: honour Cache-Control on cache entries #46
#48 — cloudflare: per-entrypoint Workers caching #47

Adds a dedicated cache handler function and the cache override that reaches it.

  • adapters/cache-adapter.ts exposes GET/PUT/DELETE /cache/* and POST /cache/revalidate-tags through createGenericHandler, backed by the IncrementalCache, TagCache and CDNInvalidationHandler overrides.
  • build/createCacheBundle.ts bundles it to .open-next/cache-function, wired into buildAdapter behind a new skipCache option and reported as cacheFunction in the OpenNext output.
  • A new top level cacheHandler config option carries the overrides for that function.
  • Three cache implementations: fetch, local and dummy, sharing the wire format in utils/cache-get.ts — entry metadata in headers, payload in the body.

Purely additive. Nothing is removed and no behaviour changes: default.override.incrementalCache and default.override.tagCache keep working exactly as before. The switchover happens in PR 3 of the stack.

…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
…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.
@pkg-pr-new

pkg-pr-new Bot commented Aug 16, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/opennextjs/adapters-api/@opennextjs/aws@53d1226
npm i https://pkg.pr.new/opennextjs/adapters-api/@opennextjs/cloudflare@53d1226
npm i https://pkg.pr.new/opennextjs/adapters-api/@opennextjs/core@53d1226

commit: 53d1226

@conico974 conico974 mentioned this pull request Aug 16, 2026
2 tasks

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 5 potential issues.

View 3 additional findings in Devin Review.

Open in Devin Review

Comment thread packages/core/src/overrides/cache/fetch.ts Outdated
Comment thread packages/core/src/adapters/cache-adapter.ts Outdated
Comment thread packages/core/src/adapters/cache-adapter.ts Outdated
Comment on lines +349 to +354
cacheFunction: config.dangerous?.disableIncrementalCache
? undefined
: {
handler: indexHandler,
bundle: ".open-next/cache-function",
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +74 to +100
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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟨 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.

Open in Devin Review

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.
@conico974
conico974 force-pushed the conico/cache-1-core-fn branch from 467f312 to c200d7a Compare August 16, 2026 13:47
Base automatically changed from conico/share-build to main August 28, 2026 10:57
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.
Copilot AI lite review requested due to automatic review settings September 18, 2026 09:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 value in buildCacheGetResponse, which throws for the primitive and turns the key into a repeated 500; validate that value is 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.get propagates a missing-object error and fs-dev.get propagates ENOENT), 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 with x-opennext-cache-found: false (or make the providers return null) 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 revalidates cacheHandler.tagCache inside the separate function. With the documented configuration (for example, cache: "fetch" plus only cacheHandler.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 cacheHandler extends DefaultFunctionOptions, cacheHandler.install is a supported configuration, but this bundle never calls installDependencies (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-function loads it. Install config.cacheHandler?.install after esbuild.
	);

packages/core/src/types/open-next.ts:484

  • cacheHandler exposes the same wrapper/converter override surface as the other function configs, but validateConfig never 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. Add validateFunctionOptions(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.

Comment thread packages/core/src/adapters/cache-handler.ts Outdated
Comment thread packages/core/src/adapters/middleware.ts
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.
Comment thread packages/aws/src/adapter.ts Outdated
@vicb
vicb added this pull request to stack #52 September 21, 2026 07:15
@vicb
vicb merged commit 59b59f7 into main Sep 21, 2026
8 checks passed
@vicb
vicb deleted the conico/cache-1-core-fn branch September 21, 2026 11:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants