Skip to content

feat: add viewerRequestFunctionCode to run code on every viewer request - #311

Merged
bestickley merged 19 commits into
mainfrom
feat/viewer-request-function-code
Oct 14, 2026
Merged

bestickley merged 19 commits into
mainfrom
feat/viewer-request-function-code

Conversation

@bestickley

@bestickley bestickley commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Closes #310.

Problem

NextjsGlobalFunctions puts its own CloudFront Function on viewer-request for the dynamic and _next/image* behaviors (overwrites X-Forwarded-Host, redirects repeated slashes), and since 0.7 an override claiming viewer-request throws at synth. That left HTTP Basic auth (and geo-gating, etc.) with no supported home:

  • viewer-response can't gate: it runs after the origin.
  • Middleware can't see Authorization (OAC signs the Function URL request with it), and _next/static / public/ never reach the server.

Change

New viewerRequestFunctionCode?: FunctionCode prop on NextjsGlobalFunctions, NextjsGlobalContainers and NextjsDistribution. The code must define viewerRequest(event), as a function (sync or async) or a var/let/const. It is a CloudFront handler in all but name: return a response to answer the viewer, or the request (or nothing) to carry on.

It is spliced into the viewer-request function of every behavior cdk-nextjs adds:

Behavior Function
default / basePath, function groups, _next/image* (Global Functions) cdk-nextjs's existing function; user code runs first, so it can't undo the X-Forwarded-Host overwrite, and it also runs before the // → 308 redirect
same, Global Containers one shared pass-through function, ViewerRequestFn (containers needed none before), also used for the static behaviors below
_next/static*, public/* the same shared pass-through function (ViewerRequestFn)
<assetPrefix>/_next/static* the existing rewrite function, which replaces the static pass-through there

Runtime and KeyValueStore:

  • Every cdk-nextjs CloudFront Function now runs on cloudfront-js-2.0 with an async handler, hook or not. That includes the existing x-forwarded-host/308 function and the assetPrefix rewrite. The hook is await viewerRequest(event), so viewerRequest can be sync or async. cdk-nextjs's function code is rewritten in the ES6 the 2.0 runtime documents (const/let, arrow functions; no destructuring or spread). Existing deployments see an in-place update of those functions on the next deploy.
  • New viewerRequestKeyValueStore?: IKeyValueStore (same three constructs) is associated with each of those functions. cf.kvs() reads it with no ID, and the user code goes first in the file, which is where CloudFront requires import cf from "cloudfront". Credentials live in the store, not in the template. The README shows the put-key command. The example wraps kvs.get in try/catch, so a store with no basic-auth key yet answers 401, and deletes the verified authorization header so it isn't forwarded to the origin.
  • Setting a store without the code throws.

Synth-time checks:

  • the code defines viewerRequest and doesn't use the name handler anywhere, comments and strings included: an assignment would replace cdk-nextjs's handler and a let/const/class would be a syntax error, and telling those from a harmless local takes a parser;
  • the combined function fits CloudFront's 10 KB limit (code with an unresolved CDK token gets a synth warning instead);
  • an overrides.*BehaviorOptions.functionAssociations viewer-request entry on a behavior the prop now covers throws, pointing at the prop. The existing Global Functions error now points there too, rather than at viewer-response/middleware;
  • overrides.distributionProps.defaultBehavior throws when the prop is set and there's no basePath, since it would replace the default behavior and its viewer-request function; customize it through overrides.dynamicBehaviorOptions.

With the prop unset, no function or association is added to static behaviors, and the existing functions differ only in runtime, the async keyword, and having their comments and indentation dropped from the deployed code (they counted toward the 10 KB limit).

Code layout:

  • The CloudFront Function bodies (x-forwarded-host/308, pass-through, assetPrefix rewrite) and the splicing/checks of viewerRequestFunctionCode live in src/utils/viewer-request-functions.ts; NextjsDistribution only creates and associates the functions. Further extractions from nextjs-distribution.ts are tracked in refactor: extract pure helpers out of nextjs-distribution.ts #313.
  • The README section is the full write-up; the prop JSDoc states the contract and links to it.

Not covered (documented)

  • A bring-your-own distribution's default behavior.

Testing

  • 34 new tests in src/nextjs-distribution.test.ts that run the synthesized functions in node:vm, with a fake cloudfront module standing in for cf.kvs(): 401/pass-through on every behavior for both Global types, one shared pass-through function on Global Containers, viewerRequest as a var/const, auth before the 308, X-Forwarded-Host still overwritten, assetPrefix rewrite, credentials read from the store, store association on every hooked function, 2.0 on every function with and without the hook, no added functions when unset, dedented deployed code, the token warning, and each synth error (static and Containers dynamic/image overrides, defaultBehavior and its basePath exemption, store without code, missing viewerRequest, eight handler forms, over 10 KB). The existing redirect/x-forwarded-host suite now awaits the async handler.
  • 11 unit tests in src/utils/viewer-request-functions.test.ts for the handler builders, the splice and the checks.
  • pnpm compile, pnpm projen (regenerates OptionalNextjsDistributionProps), pnpm eslint, pnpm test: 1176/1176 passing.
  • Needs a real deploy to confirm the 2.0 behavior of the existing redirect/query-string code, and cf.kvs() with no ID.
  • Not deployed. No e2e/harness run yet.

NextjsGlobalFunctions owns the viewer-request CloudFront Function on its
dynamic and image behaviors, and rejects overrides that claim that event
type, which left HTTP Basic auth and similar edge gates with nowhere to
go. The new prop on NextjsGlobalFunctions and NextjsGlobalContainers is
spliced ahead of cdk-nextjs's own logic, and runs on every behavior
cdk-nextjs adds, static ones included.

Closes #310
@github-actions

Copy link
Copy Markdown
Contributor

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

github-actions Bot and others added 7 commits October 13, 2026 12:29
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…onal KeyValueStore

Functions the hook runs in move to cloudfront-js-2.0 with an async
handler that awaits viewerRequest, so user code can be async and read a
store through cf.kvs(). The new viewerRequestKeyValueStore prop is
associated with each of them, keeping credentials out of the template.
Without the hook, cdk-nextjs's own function stays on cloudfront-js-1.0.
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
The x-forwarded-host/redirect function and the assetPrefix rewrite now
run on cloudfront-js-2.0 with async handlers whether or not
viewerRequestFunctionCode is set, so there is one runtime and one
handler shape. The ES5.1 code runs unchanged; existing deployments see
an in-place update of those functions.
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@bestickley
bestickley marked this pull request as ready for review October 13, 2026 15:11
- R1-1: refuse user code that assigns `handler` (var/let/const/class, bare or globalThis/this assignment), not just a `function handler` declaration
- R1-2: accept `viewerRequest` defined as `var`/`let`/`const`
- R1-3: wrap `kvs.get` in try/catch in the JSDoc example, as the README does
- R1-4: move the CloudFrontFn notes out of the deployed code so they don't count toward the 10 KB limit
- R1-5: share one pass-through CloudFront Function between the dynamic and static behaviors
- R1-6: render and check `viewerRequestFunctionCode` once, in the constructor
- R1-7: route the assetPrefix viewer-request check through assertViewerRequestUnclaimed
- R2-1: rename cdk-nextjs's handler when user code shares the file and end it with `handler = cdkNextjsHandler`, so no top-level assignment (var, destructuring, globalThis["handler"]) can replace it
- R2-2: refuse only `handler` declarations (function, let/const/class); var and assignments are now harmless, so locals using them pass
- R2-3: refuse a `function* handler` declaration
- R2-4: accept `viewerRequest` declared after another variable in one var/let/const
- R2-5: missing-viewerRequest error names the var/let/const forms
- R2-6: prop JSDocs (and the generated struct) document the const form
- R2-7: shared pass-through test checks every behavior references the one function
- R2-8: gate the pass-through on the rendered code, not the prop
- R3-1/R3-4/R3-5/R3-6: drop the renamed handler + trailing reassignment (a user `cdkNextjsHandler` assignment still replaced ours, and a user `var handler` beside our `function handler` is a SyntaxError if the file parses as a module); keep `async function handler` and refuse any use of the name `handler` in user code instead
- R3-2: comma-list and destructured `let`/`const handler` are refused (by the same check)
- R3-3: the viewerRequest check no longer accepts `===` comparisons or member assignments
- R3-7: root-construct JSDocs name var/let/const
- R4-1: refuse `overrides.distributionProps.defaultBehavior` when `viewerRequestFunctionCode` is set, since it replaces the default behavior and drops the hook from pages and API routes
- R4-2: warn instead of size-checking when the combined code contains an unresolved CDK token
- R4-3: README `put-key` strips the newlines GNU `base64` wraps with
- R4-4: README and JSDoc Basic-auth examples delete the verified `authorization` header so it isn't forwarded to the origin
- R4-5: dedent cdk-nextjs's own handler code so its indentation doesn't count toward the 10 KB limit
- S4-4: test the Global Containers dynamic/image viewer-request override error
- R5-1: allow `overrides.distributionProps.defaultBehavior` under a `basePath`, where cdk-nextjs's own behaviors serve the app
- R5-2: dedent cdk-nextjs's handler after the hook is spliced in, so the hook's indentation doesn't count toward 10 KB either
- S5-3: README notes code with a CDK token gets a warning, not a size check
@bestickley

Copy link
Copy Markdown
Contributor Author

Review to convergence: 5 rounds

Each round, fresh reviewers checked for bugs (/code-review high) and, on full passes, checked the branch against this PR's spec. Accepted findings were fixed, verified (pnpm compile, pnpm eslint, pnpm jest) and committed. It converged on round 5, a full pass with no medium or high findings. Round 5's low fixes (f126e92) haven't been reviewed. pnpm test: 1165/1165.

Round Range Raw (bugs/spec) Blocking Fixed Escalated Commit
1 full 7/1 1 7 1 a2aeb72
2 delta 8/– 1 8 0 03071f3
3 delta 7/– 2 7 1 0b79604
4 full 5/7 1 6 7 84c0d54
5 full 2/4 0 3 3 f126e92

Fixes

User code replacing cdk-nextjs's handler (R1-1, R2-1, R3-1..6). var handler = …, destructuring and globalThis["handler"] = … all slipped past the function handler check. Each would replace our handler and drop the X-Forwarded-Host overwrite. Two approaches were tried in between: a wider regex, then renaming ours and reassigning it at the end of the file. The rename was bypassable, and if CloudFront parses the file as an ES module it would be a SyntaxError. Final approach: ours stays async function handler, and synth refuses user code that uses the word handler anywhere.

Coverage

  • R4-1/R5-1: with the hook set and no basePath, overrides.distributionProps.defaultBehavior silently dropped the hook from pages and API routes. It now throws.
  • S4-4: added a test for the Global Containers dynamic/image override error.

Validation

  • R1-2/R2-4/R3-3: viewerRequest may be var/let/const, including after another variable in the same declaration. === comparisons and member assignments no longer count as defining it.
  • R2-3: function* handler is refused.
  • R2-5/R2-6/R3-7: error messages and JSDocs updated to match.
  • R1-6/R2-8: the code is rendered and checked once.

Size budget

  • R1-4: cdk-nextjs's comments moved out of the deployed CloudFrontFn, about 2.2 KB.
  • R4-5/R5-2: its indentation is dropped too, hook text included.
  • R4-2: code with an unresolved CDK token gets a synth warning instead of a size check, since its size isn't known until deploy.

Functions

  • R1-5: one shared pass-through ViewerRequestFn for the static and (on Containers) dynamic behaviors.
  • R1-7: the assetPrefix override check uses the shared assertion.

Docs and examples

  • R1-3: kvs.get is wrapped in try/catch.
  • R4-4: the example deletes the verified authorization header, so Containers doesn't forward it.
  • R4-3: base64 | tr -d '\n', because GNU wraps at 76 columns.
  • R2-7: the shared-function test now checks each behavior's association.

Escalations, all resolved by the author: keep as is

  • R2-2: refusing the word handler also refuses harmless locals and mentions in comments or strings. We accept that cost.
  • S4-1: the assetPrefix override error points at viewerRequestFunctionCode even when the prop is unset.
  • S5-1: the defaultBehavior throw (exempt under a basePath) goes beyond the spec's list. A bring-your-own distribution stays documented as not covered.
  • S1-1, S4-2..7, S5-2/S5-4: the PR body was stale. It has been updated.

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
bestickley and others added 5 commits October 13, 2026 14:05
…Code splicing to utils/viewer-request-functions
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@bestickley
bestickley disabled auto-merge October 13, 2026 18:51
@bestickley bestickley self-assigned this Oct 13, 2026
@bestickley
bestickley merged commit c97c014 into main Oct 14, 2026
15 checks passed
@bestickley
bestickley deleted the feat/viewer-request-function-code branch October 14, 2026 02:37
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.

How should HTTP Basic auth be added on NextjsGlobalFunctions in 0.7?

1 participant