@timber-js/app 0.2.0-alpha.197 → 0.2.0-alpha.199
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/{actions-BS-m5SLv.js → actions-d1hCqnU3.js} +35 -8
- package/dist/_chunks/actions-d1hCqnU3.js.map +1 -0
- package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -1
- package/dist/_chunks/{cache-api-DqzgTEqk.js → cache-api-ByagcC-J.js} +2 -2
- package/dist/_chunks/{cache-api-DqzgTEqk.js.map → cache-api-ByagcC-J.js.map} +1 -1
- package/dist/_chunks/canonicalize-CgHoscYO.js +66 -0
- package/dist/_chunks/canonicalize-CgHoscYO.js.map +1 -0
- package/dist/_chunks/{chains-CZG7E5zg.js → chains-Bpb0W4ax.js} +3 -3
- package/dist/_chunks/{chains-CZG7E5zg.js.map → chains-Bpb0W4ax.js.map} +1 -1
- package/dist/_chunks/{cli-check-bE3H5Bjr.js → cli-check-D6VolrDV.js} +3 -3
- package/dist/_chunks/{cli-check-bE3H5Bjr.js.map → cli-check-D6VolrDV.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-DTy_-Msq.js → cli-schema-sync-D6rO-VcS.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-DTy_-Msq.js.map → cli-schema-sync-D6rO-VcS.js.map} +1 -1
- package/dist/_chunks/{convention-lint-n3RJLgww.js → convention-lint-fRkwVwEH.js} +27 -4
- package/dist/_chunks/convention-lint-fRkwVwEH.js.map +1 -0
- package/dist/_chunks/error-boundary-BfPHZjm0.js +1050 -0
- package/dist/_chunks/error-boundary-BfPHZjm0.js.map +1 -0
- package/dist/_chunks/{live-graph-BXDsdzBv.js → live-graph-D_2D32Ad.js} +3 -3
- package/dist/_chunks/{live-graph-BXDsdzBv.js.map → live-graph-D_2D32Ad.js.map} +1 -1
- package/dist/_chunks/{logger-DDirEsn7.js → logger-uLBuGKDI.js} +471 -440
- package/dist/_chunks/logger-uLBuGKDI.js.map +1 -0
- package/dist/_chunks/{navigation-root-B00jjGd5.js → navigation-context-D0TU0Jog.js} +3 -101
- package/dist/_chunks/navigation-context-D0TU0Jog.js.map +1 -0
- package/dist/_chunks/navigation-root-mHSK9psY.js +126 -0
- package/dist/_chunks/{navigation-root-B00jjGd5.js.map → navigation-root-mHSK9psY.js.map} +1 -1
- package/dist/_chunks/{poison-scan-BoDLgbix.js → poison-scan-Bm9Yyqk9.js} +2 -2
- package/dist/_chunks/{poison-scan-BoDLgbix.js.map → poison-scan-Bm9Yyqk9.js.map} +1 -1
- package/dist/_chunks/{scanner-tdFPvDYi.js → scanner-AiazgH_f.js} +6 -5
- package/dist/_chunks/scanner-AiazgH_f.js.map +1 -0
- package/dist/_chunks/{segment-keys-BhqoHiLc.js → segment-keys-lqtdookO.js} +2 -65
- package/dist/_chunks/segment-keys-lqtdookO.js.map +1 -0
- package/dist/_chunks/{ssr-data-BQGhTPAK.js → ssr-data-D6T6Y3ef.js} +4 -26
- package/dist/_chunks/ssr-data-D6T6Y3ef.js.map +1 -0
- package/dist/_chunks/state-FippDgxN.js +52 -0
- package/dist/_chunks/state-FippDgxN.js.map +1 -0
- package/dist/_chunks/status-page-marker-DwQBrLBz.js +496 -0
- package/dist/_chunks/status-page-marker-DwQBrLBz.js.map +1 -0
- package/dist/_chunks/{walkers-DNX05dC0.js → walkers-B6XUtmqK.js} +2 -2
- package/dist/_chunks/{walkers-DNX05dC0.js.map → walkers-B6XUtmqK.js.map} +1 -1
- package/dist/analyze/crawl-entry.js +2 -2
- package/dist/analyze/graph-command.js +2 -2
- package/dist/cache/index.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/client/browser-entry/action-dispatch.d.ts +6 -4
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/action-queue.d.ts +44 -0
- package/dist/client/browser-entry/action-queue.d.ts.map +1 -0
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/deny-last-resort.d.ts +29 -0
- package/dist/client/deny-last-resort.d.ts.map +1 -0
- package/dist/client/error-boundary.d.ts +48 -2
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +2 -2
- package/dist/client/history.d.ts +21 -2
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/index.js +34 -14
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +1 -0
- package/dist/client/internal.d.ts.map +1 -1
- package/dist/client/internal.js +272 -1225
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-commit.d.ts +12 -19
- package/dist/client/navigation-commit.d.ts.map +1 -1
- package/dist/client/navigation-transition.d.ts +62 -11
- package/dist/client/navigation-transition.d.ts.map +1 -1
- package/dist/client/router-effects.d.ts +9 -8
- package/dist/client/router-effects.d.ts.map +1 -1
- package/dist/client/router-lifecycle.d.ts +60 -17
- package/dist/client/router-lifecycle.d.ts.map +1 -1
- package/dist/client/router-pipeline.d.ts +7 -4
- package/dist/client/router-pipeline.d.ts.map +1 -1
- package/dist/client/router-types.d.ts +63 -8
- package/dist/client/router-types.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +0 -9
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +23 -8
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/state.d.ts +16 -0
- package/dist/client/state.d.ts.map +1 -1
- package/dist/client/status-page-marker.d.ts +25 -0
- package/dist/client/status-page-marker.d.ts.map +1 -0
- package/dist/cookies/index.js +1 -1
- package/dist/dev-tools/holding-server.d.ts +4 -17
- package/dist/dev-tools/holding-server.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +74 -175
- package/dist/index.js.map +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/routing/codegen-write.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/routing/interception.d.ts +2 -2
- package/dist/routing/slot-placement.d.ts +2 -2
- package/dist/server/access-gate.d.ts +73 -1
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/actions.d.ts +16 -1
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +3 -9
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/children-interception.d.ts +1 -1
- package/dist/server/default-status-page.d.ts +2 -2
- package/dist/server/default-status-page.d.ts.map +1 -1
- package/dist/server/deny-boundary.d.ts +15 -9
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +21 -4
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/error-response-headers.d.ts +3 -0
- package/dist/server/error-response-headers.d.ts.map +1 -0
- package/dist/server/index.js +3 -3
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.d.ts +1 -2
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +2339 -2506
- package/dist/server/internal.js.map +1 -1
- package/dist/server/metadata-collector.d.ts +2 -5
- package/dist/server/metadata-collector.d.ts.map +1 -1
- package/dist/server/param-coercion.d.ts +10 -3
- package/dist/server/param-coercion.d.ts.map +1 -1
- package/dist/server/pipeline-outcome.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts +11 -0
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/port-resolution.d.ts +3 -89
- package/dist/server/port-resolution.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +38 -10
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/response-cache-policy.d.ts +3 -0
- package/dist/server/response-cache-policy.d.ts.map +1 -0
- package/dist/server/route-element-builder.d.ts +11 -41
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/route-element-helpers.d.ts +12 -0
- package/dist/server/route-element-helpers.d.ts.map +1 -0
- package/dist/server/route-module-loader.d.ts +37 -0
- package/dist/server/route-module-loader.d.ts.map +1 -0
- package/dist/server/rsc-cache-key-guard.d.ts.map +1 -1
- package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts +22 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +4 -11
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/rsc-error-envelope.d.ts +11 -0
- package/dist/server/rsc-error-envelope.d.ts.map +1 -0
- package/dist/server/skippable-prefix.d.ts +18 -15
- package/dist/server/skippable-prefix.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/slot-subtree-contain.d.ts +54 -0
- package/dist/server/slot-subtree-contain.d.ts.map +1 -0
- package/dist/server/stream-utils.d.ts.map +1 -1
- package/dist/server/utils/element-type.d.ts +10 -0
- package/dist/server/utils/element-type.d.ts.map +1 -1
- package/dist/shared/rsc-error-envelope.d.ts +0 -9
- package/dist/shared/rsc-error-envelope.d.ts.map +1 -1
- package/dist/shared/status-reason-phrase.d.ts +26 -0
- package/dist/shared/status-reason-phrase.d.ts.map +1 -0
- package/docs/api/30-api-server.mdx +4 -2
- package/docs/api/31-api-client.mdx +5 -1
- package/docs/api/36-cli.mdx +5 -3
- package/docs/learn/12-error-handling.mdx +5 -1
- package/package.json +10 -10
- package/src/client/browser-entry/action-dispatch.ts +166 -99
- package/src/client/browser-entry/action-queue.ts +90 -0
- package/src/client/browser-entry/router-init.ts +60 -35
- package/src/client/deny-last-resort.tsx +54 -0
- package/src/client/error-boundary.tsx +144 -42
- package/src/client/history.ts +52 -3
- package/src/client/internal.ts +1 -0
- package/src/client/link.tsx +70 -35
- package/src/client/navigation-commit.ts +79 -27
- package/src/client/navigation-transition.ts +176 -127
- package/src/client/router-effects.ts +14 -17
- package/src/client/router-lifecycle.ts +181 -115
- package/src/client/router-pipeline.ts +94 -71
- package/src/client/router-types.ts +61 -7
- package/src/client/router.ts +147 -74
- package/src/client/rsc-fetch.ts +0 -13
- package/src/client/segment-cache.ts +43 -10
- package/src/client/state.ts +26 -0
- package/src/client/status-page-marker.tsx +32 -0
- package/src/dev-tools/holding-server.ts +4 -17
- package/src/index.ts +18 -34
- package/src/plugins/dev-server.ts +2 -1
- package/src/react-canary.d.ts +2 -0
- package/src/routing/codegen-write.ts +2 -0
- package/src/routing/interception.ts +2 -2
- package/src/routing/slot-placement.ts +2 -2
- package/src/server/access-gate.tsx +89 -21
- package/src/server/action-client.ts +2 -2
- package/src/server/action-handler.ts +23 -10
- package/src/server/actions.ts +81 -34
- package/src/server/als-registry.ts +3 -9
- package/src/server/children-interception.ts +1 -1
- package/src/server/default-status-page.ts +7 -47
- package/src/server/deny-boundary.ts +45 -28
- package/src/server/deny-renderer.ts +6 -2
- package/src/server/error-boundary-wrapper.ts +23 -4
- package/src/server/error-response-headers.ts +18 -0
- package/src/server/internal.ts +2 -10
- package/src/server/metadata-collector.ts +3 -18
- package/src/server/param-coercion.ts +13 -4
- package/src/server/pipeline-outcome.ts +35 -13
- package/src/server/pipeline-phases.ts +22 -16
- package/src/server/port-resolution.ts +3 -165
- package/src/server/prebuilt-builder.ts +4 -4
- package/src/server/primitives.ts +75 -11
- package/src/server/response-cache-policy.ts +45 -0
- package/src/server/route-element-builder.ts +149 -412
- package/src/server/route-element-helpers.ts +37 -0
- package/src/server/route-handler.ts +2 -2
- package/src/server/route-module-loader.ts +161 -0
- package/src/server/rsc-cache-key-guard.ts +2 -42
- package/src/server/rsc-entry/action-middleware-runner.ts +4 -4
- package/src/server/rsc-entry/api-handler.ts +5 -5
- package/src/server/rsc-entry/error-renderer.ts +3 -4
- package/src/server/rsc-entry/helpers.ts +1 -1
- package/src/server/rsc-entry/index.ts +3 -3
- package/src/server/rsc-entry/render-route.ts +4 -8
- package/src/server/rsc-entry/rsc-payload.ts +59 -42
- package/src/server/rsc-entry/rsc-stream.ts +48 -27
- package/src/server/rsc-entry/ssr-renderer.ts +6 -10
- package/src/server/rsc-error-envelope.ts +18 -0
- package/src/server/skippable-prefix.ts +105 -7
- package/src/server/slot-resolver.ts +43 -12
- package/src/server/slot-subtree-contain.ts +255 -0
- package/src/server/stream-utils.ts +12 -8
- package/src/server/utils/element-type.ts +18 -2
- package/src/shared/rsc-error-envelope.ts +0 -15
- package/src/shared/status-reason-phrase.ts +61 -0
- package/dist/_chunks/actions-BS-m5SLv.js.map +0 -1
- package/dist/_chunks/convention-lint-n3RJLgww.js.map +0 -1
- package/dist/_chunks/error-boundary-BvRCCmbN.js +0 -353
- package/dist/_chunks/error-boundary-BvRCCmbN.js.map +0 -1
- package/dist/_chunks/logger-DDirEsn7.js.map +0 -1
- package/dist/_chunks/mdx-file-CXyHGUpS.js +0 -25
- package/dist/_chunks/mdx-file-CXyHGUpS.js.map +0 -1
- package/dist/_chunks/router-ref-8gr8qsxN.js +0 -28
- package/dist/_chunks/router-ref-8gr8qsxN.js.map +0 -1
- package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js +0 -40
- package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js.map +0 -1
- package/dist/_chunks/scanner-tdFPvDYi.js.map +0 -1
- package/dist/_chunks/segment-keys-BhqoHiLc.js.map +0 -1
- package/dist/_chunks/ssr-data-BQGhTPAK.js.map +0 -1
- package/dist/server/tree-builder.d.ts +0 -150
- package/dist/server/tree-builder.d.ts.map +0 -1
- package/src/server/tree-builder.ts +0 -313
|
@@ -37,8 +37,9 @@ import { createElement } from 'react';
|
|
|
37
37
|
|
|
38
38
|
import { ErrorReconstituter } from '../client/error-reconstituter.tsx';
|
|
39
39
|
import type { SerializableError } from '../client/error-reconstituter.tsx';
|
|
40
|
+
import { StatusPageMarker } from '../client/status-page-marker.tsx';
|
|
40
41
|
import { requestContextAls } from './als-registry.ts';
|
|
41
|
-
import { DenySignal } from './primitives.ts';
|
|
42
|
+
import { isDenySignal, DenySignal } from './primitives.ts';
|
|
42
43
|
import { loadModule } from './safe-load.ts';
|
|
43
44
|
import type { ManifestLoader } from './safe-load.ts';
|
|
44
45
|
import { withSpan } from './tracing.ts';
|
|
@@ -252,22 +253,32 @@ export async function buildDenyPageChain(
|
|
|
252
253
|
* `resolve5xx`. See TIM-1360.
|
|
253
254
|
*/
|
|
254
255
|
export function matchDenyEntry(chain: DenyPageEntry[], status: number): DenyPageEntry | null {
|
|
255
|
-
|
|
256
|
+
return matchDenyEntries(chain, status)[0] ?? null;
|
|
257
|
+
}
|
|
256
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Return ALL deny page entries whose status filter matches, in traversal
|
|
261
|
+
* order (best match first). `matchDenyEntry` returns only the first;
|
|
262
|
+
* late addressing (TIM-1450) needs the full list so the client can find
|
|
263
|
+
* the first *reachable* owner via its ancestry context.
|
|
264
|
+
*/
|
|
265
|
+
export function matchDenyEntries(chain: DenyPageEntry[], status: number): DenyPageEntry[] {
|
|
266
|
+
if (status >= 500) return matchDenyEntries5xx(chain, status);
|
|
267
|
+
|
|
268
|
+
const matches: DenyPageEntry[] = [];
|
|
257
269
|
for (const entry of chain) {
|
|
258
|
-
if (entry.status === status)
|
|
259
|
-
if (entry.status === 400 && status >= 400 && status <= 499)
|
|
260
|
-
if (entry.status === null)
|
|
270
|
+
if (entry.status === status) matches.push(entry);
|
|
271
|
+
else if (entry.status === 400 && status >= 400 && status <= 499) matches.push(entry);
|
|
272
|
+
else if (entry.status === null) matches.push(entry);
|
|
261
273
|
}
|
|
262
|
-
return
|
|
274
|
+
return matches;
|
|
263
275
|
}
|
|
264
276
|
|
|
265
277
|
/**
|
|
266
278
|
* 5xx per-segment walk: at each segment (inner→outer), check status files
|
|
267
279
|
* then error.tsx before moving outward. Mirrors `resolve5xx`.
|
|
268
280
|
*/
|
|
269
|
-
function
|
|
270
|
-
// Collect unique segment indices, sorted descending (inner→outer).
|
|
281
|
+
function matchDenyEntries5xx(chain: DenyPageEntry[], status: number): DenyPageEntry[] {
|
|
271
282
|
const segmentIndices: number[] = [];
|
|
272
283
|
for (const entry of chain) {
|
|
273
284
|
if (!segmentIndices.includes(entry.segmentIndex)) {
|
|
@@ -276,20 +287,19 @@ function matchDenyEntry5xx(chain: DenyPageEntry[], status: number): DenyPageEntr
|
|
|
276
287
|
}
|
|
277
288
|
segmentIndices.sort((a, b) => b - a);
|
|
278
289
|
|
|
290
|
+
const matches: DenyPageEntry[] = [];
|
|
279
291
|
for (const seg of segmentIndices) {
|
|
280
|
-
// Status files at this segment (exact then category).
|
|
281
292
|
for (const entry of chain) {
|
|
282
293
|
if (entry.segmentIndex !== seg || entry.kind === 'error') continue;
|
|
283
|
-
if (entry.status === status)
|
|
284
|
-
if (entry.status === 500 && status >= 500 && status <= 599)
|
|
294
|
+
if (entry.status === status) matches.push(entry);
|
|
295
|
+
else if (entry.status === 500 && status >= 500 && status <= 599) matches.push(entry);
|
|
285
296
|
}
|
|
286
|
-
// error.tsx at this segment.
|
|
287
297
|
for (const entry of chain) {
|
|
288
298
|
if (entry.segmentIndex !== seg || entry.kind !== 'error') continue;
|
|
289
|
-
|
|
299
|
+
matches.push(entry);
|
|
290
300
|
}
|
|
291
301
|
}
|
|
292
|
-
return
|
|
302
|
+
return matches;
|
|
293
303
|
}
|
|
294
304
|
|
|
295
305
|
/**
|
|
@@ -355,7 +365,19 @@ export function renderDenyEntry(
|
|
|
355
365
|
data: unknown
|
|
356
366
|
): React.ReactElement {
|
|
357
367
|
const h = createElement as (...args: unknown[]) => React.ReactElement;
|
|
368
|
+
// Every deny page the server places — in-tree at the owning catch site, or
|
|
369
|
+
// through the fallback renderers — is a status page the client's segment
|
|
370
|
+
// cache must not learn from, and the marker is how it says so on commit.
|
|
371
|
+
// See client/status-page-marker.tsx (TIM-1495).
|
|
372
|
+
return h(StatusPageMarker, null, renderDenyPage(entry, status, data, h));
|
|
373
|
+
}
|
|
358
374
|
|
|
375
|
+
function renderDenyPage(
|
|
376
|
+
entry: DenyEntryShape,
|
|
377
|
+
status: number,
|
|
378
|
+
data: unknown,
|
|
379
|
+
h: (...args: unknown[]) => React.ReactElement
|
|
380
|
+
): React.ReactElement {
|
|
359
381
|
if (entry.kind !== 'error') {
|
|
360
382
|
return h(entry.component, { status, dangerouslyPassData: data });
|
|
361
383
|
}
|
|
@@ -391,15 +413,16 @@ export function renderDenyEntry(
|
|
|
391
413
|
* file, bare when nothing matched at all.
|
|
392
414
|
*
|
|
393
415
|
* `setDenyStatus` runs on the hoist path too, and must run *before* the
|
|
394
|
-
* throw: on the
|
|
395
|
-
*
|
|
396
|
-
*
|
|
416
|
+
* throw: on the document path the hoisted row fails the Fizz shell and the
|
|
417
|
+
* re-render fallback serves the page, and the ALS status is what the
|
|
418
|
+
* pipeline commits for it. The RSC payload path never reads it — a Flight
|
|
419
|
+
* payload response is always 200 and the row itself carries the deny (TIM-1495).
|
|
397
420
|
*
|
|
398
421
|
* Either branch leaves part of the matched chain unmounted — hoisting sends
|
|
399
422
|
* the render to a boundary that discards everything below it, and a layout
|
|
400
423
|
* that denies returns the deny page instead of its own child outlet. What the
|
|
401
|
-
*
|
|
402
|
-
*
|
|
424
|
+
* client then learns about the tree is decided by the boundary that renders
|
|
425
|
+
* the page, at the commit that puts it on screen (`navigation-commit.ts`).
|
|
403
426
|
*/
|
|
404
427
|
export function handleCaughtDeny(
|
|
405
428
|
chain: DenyPageEntry[] | undefined,
|
|
@@ -414,7 +437,7 @@ export function handleCaughtDeny(
|
|
|
414
437
|
}
|
|
415
438
|
if (placement?.kind === 'hoist') {
|
|
416
439
|
setDenyStatus(error.status);
|
|
417
|
-
error.
|
|
440
|
+
error.owners = [placement.ownerKey];
|
|
418
441
|
}
|
|
419
442
|
throw error;
|
|
420
443
|
}
|
|
@@ -440,7 +463,6 @@ export async function PageDenyBoundary({
|
|
|
440
463
|
route,
|
|
441
464
|
denyPages,
|
|
442
465
|
segmentKey,
|
|
443
|
-
onSettled,
|
|
444
466
|
}: {
|
|
445
467
|
/** The page server component function. */
|
|
446
468
|
Page: (...args: unknown[]) => unknown;
|
|
@@ -454,19 +476,15 @@ export async function PageDenyBoundary({
|
|
|
454
476
|
* chain matched further up is hoisted to its owner.
|
|
455
477
|
*/
|
|
456
478
|
segmentKey: string;
|
|
457
|
-
/** Called when the page component finishes (success or deny). See TIM-1045. */
|
|
458
|
-
onSettled?: () => void;
|
|
459
479
|
}): Promise<React.ReactElement> {
|
|
460
480
|
try {
|
|
461
481
|
const result = await withSpan('timber.page', { 'timber.route': route }, () => Page({}));
|
|
462
482
|
return result as React.ReactElement;
|
|
463
483
|
} catch (error: unknown) {
|
|
464
|
-
if (error
|
|
484
|
+
if (isDenySignal(error)) {
|
|
465
485
|
return handleCaughtDeny(denyPages, error, segmentKey);
|
|
466
486
|
}
|
|
467
487
|
throw error;
|
|
468
|
-
} finally {
|
|
469
|
-
onSettled?.();
|
|
470
488
|
}
|
|
471
489
|
}
|
|
472
490
|
|
|
@@ -475,13 +493,12 @@ export async function PageDenyBoundary({
|
|
|
475
493
|
/**
|
|
476
494
|
* Set the deny status in the request context ALS.
|
|
477
495
|
* Called from AccessGate / PageDenyBoundary when a DenySignal is caught.
|
|
478
|
-
* The
|
|
496
|
+
* The document path reads this after render to set the HTTP status code.
|
|
479
497
|
*/
|
|
480
498
|
export function setDenyStatus(status: number): void {
|
|
481
499
|
const store = requestContextAls.getStore();
|
|
482
500
|
if (store) {
|
|
483
501
|
store.denyStatus = status;
|
|
484
|
-
store.onDenyStatus?.();
|
|
485
502
|
}
|
|
486
503
|
}
|
|
487
504
|
|
|
@@ -32,7 +32,7 @@ import { resolveMetadata, renderMetadataToElements } from './metadata.ts';
|
|
|
32
32
|
import { headElementsToReact } from './metadata.ts';
|
|
33
33
|
import { resolveStatusFile } from './status-code-resolver.ts';
|
|
34
34
|
import { defaultStatusPageResponse } from './default-status-page.ts';
|
|
35
|
-
import { rscErrorEnvelope } from '
|
|
35
|
+
import { rscErrorEnvelope } from './rsc-error-envelope.ts';
|
|
36
36
|
import type { ManifestSegmentNode } from './route-matcher.ts';
|
|
37
37
|
import type { RouteMatch } from './pipeline.ts';
|
|
38
38
|
import type { NavContext } from './ssr-bridge-types.ts';
|
|
@@ -327,8 +327,12 @@ export async function renderDenyPageAsRsc(
|
|
|
327
327
|
});
|
|
328
328
|
|
|
329
329
|
responseHeaders.set('content-type', `${RSC_CONTENT_TYPE}; charset=utf-8`);
|
|
330
|
+
// 200, like every Flight payload response: the deny is the page in the body, and
|
|
331
|
+
// the client router — the only reader of this status — renders what it is
|
|
332
|
+
// sent. See design/04-authorization.md §"Status codes on the RSC payload
|
|
333
|
+
// path" (TIM-1495).
|
|
330
334
|
return new Response(rscStream, {
|
|
331
|
-
status:
|
|
335
|
+
status: 200,
|
|
332
336
|
headers: responseHeaders,
|
|
333
337
|
});
|
|
334
338
|
}
|
|
@@ -7,13 +7,11 @@
|
|
|
7
7
|
*
|
|
8
8
|
* - `route-element-builder.ts` / `slot-resolver.ts` — the request-time
|
|
9
9
|
* renderer, walking the runtime manifest (`SegmentNode<ManifestFile>`).
|
|
10
|
-
* - `tree-builder.ts` — the build-time tree walker
|
|
11
|
-
* (`SegmentNode<RouteFile>`), with its loader and `createElement` injected.
|
|
12
10
|
*
|
|
13
11
|
* `errorBoundaryComponent` is injected rather than imported: `TimberErrorBoundary`
|
|
14
12
|
* is a `'use client'` module, and importing it here would pull client code into
|
|
15
|
-
* every consumer
|
|
16
|
-
* `
|
|
13
|
+
* every consumer. `tests/use-client-pipeline.test.ts` walks the import graph
|
|
14
|
+
* from this file to enforce that no `client/` module is reachable at any depth.
|
|
17
15
|
*
|
|
18
16
|
* See design/10-error-handling.md §"Status-Code File Variants".
|
|
19
17
|
*/
|
|
@@ -54,6 +52,21 @@ export interface ErrorBoundaryProps {
|
|
|
54
52
|
* every owner-addressed deny. See TIM-1356.
|
|
55
53
|
*/
|
|
56
54
|
segmentKey?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Catch denies only; re-throw every other error. Set on the copy of a
|
|
57
|
+
* segment's boundaries that sits outside its layout, which exists to give a
|
|
58
|
+
* deny thrown from the layout's own chrome a catcher at the right position
|
|
59
|
+
* without changing where a 5xx from that chrome goes. See TIM-1450.
|
|
60
|
+
*/
|
|
61
|
+
denyOnly?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* The boundary belongs to a parallel slot. A slot degrading is the page
|
|
64
|
+
* staying: its status pages never set the document status and never tell
|
|
65
|
+
* the segment cache the main route's chain is gone. Set on every boundary
|
|
66
|
+
* a slot's files declare, not only the catch-all outside them — the
|
|
67
|
+
* identity is the slot's, not the file's (codex on #1138).
|
|
68
|
+
*/
|
|
69
|
+
isSlotBoundary?: boolean;
|
|
57
70
|
children: ReactNode;
|
|
58
71
|
}
|
|
59
72
|
|
|
@@ -75,6 +88,10 @@ export interface ErrorBoundaryWrapConfig<TFile, TElement> {
|
|
|
75
88
|
errorBoundaryComponent: unknown;
|
|
76
89
|
/** Segment key stamped on every boundary this walk creates. See `ErrorBoundaryProps`. */
|
|
77
90
|
segmentKey?: string;
|
|
91
|
+
/** Stamped on every boundary this walk creates. See `ErrorBoundaryProps`. */
|
|
92
|
+
denyOnly?: boolean;
|
|
93
|
+
/** Stamped on every boundary this walk creates. See `ErrorBoundaryProps`. */
|
|
94
|
+
isSlotBoundary?: boolean;
|
|
78
95
|
}
|
|
79
96
|
|
|
80
97
|
/**
|
|
@@ -122,6 +139,8 @@ function wrapInBoundary<TFile extends BoundaryFile, TElement extends ReactNode>(
|
|
|
122
139
|
: { fallbackComponent: Component, children: element };
|
|
123
140
|
if (status !== undefined) props.status = status;
|
|
124
141
|
if (config.segmentKey !== undefined) props.segmentKey = config.segmentKey;
|
|
142
|
+
if (config.denyOnly) props.denyOnly = true;
|
|
143
|
+
if (config.isSlotBoundary) props.isSlotBoundary = true;
|
|
125
144
|
return config.createElement(config.errorBoundaryComponent, props);
|
|
126
145
|
}
|
|
127
146
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { preventResponseCaching } from './response-cache-policy.ts';
|
|
2
|
+
|
|
3
|
+
/** Preserve request-wide metadata when replacing a failed response body. */
|
|
4
|
+
export function errorResponseHeaders(responseHeaders?: Headers): Headers {
|
|
5
|
+
const headers = new Headers(responseHeaders);
|
|
6
|
+
// These describe the abandoned body, not the replacement empty/JSON body.
|
|
7
|
+
headers.delete('Content-Length');
|
|
8
|
+
headers.delete('Content-Encoding');
|
|
9
|
+
headers.delete('Transfer-Encoding');
|
|
10
|
+
// A transient failure must not inherit the success page's cache lifetime
|
|
11
|
+
// or validators, including CDN overrides of Cache-Control.
|
|
12
|
+
preventResponseCaching(headers);
|
|
13
|
+
headers.delete('ETag');
|
|
14
|
+
headers.delete('Last-Modified');
|
|
15
|
+
headers.delete('Expires');
|
|
16
|
+
headers.delete('Age');
|
|
17
|
+
return headers;
|
|
18
|
+
}
|
package/src/server/internal.ts
CHANGED
|
@@ -60,21 +60,13 @@ export type { ProxyFn, ProxyExport } from './proxy.ts';
|
|
|
60
60
|
export { runMiddleware } from './middleware-runner.ts';
|
|
61
61
|
export type { MiddlewareFn } from './middleware-runner.ts';
|
|
62
62
|
|
|
63
|
-
// ── Tree
|
|
64
|
-
export { buildElementTree } from './tree-builder.ts';
|
|
65
|
-
export type {
|
|
66
|
-
TreeBuilderConfig,
|
|
67
|
-
TreeBuildResult,
|
|
68
|
-
LoadedModule,
|
|
69
|
-
ModuleLoader,
|
|
70
|
-
AccessGateProps,
|
|
71
|
-
SlotAccessGateProps,
|
|
72
|
-
} from './tree-builder.ts';
|
|
63
|
+
// ── Element Tree Types ──────────────────────────────────────────────────
|
|
73
64
|
export type { LoadedComponent } from './utils/element-type.ts';
|
|
74
65
|
export type { ErrorBoundaryProps } from './error-boundary-wrapper.ts';
|
|
75
66
|
|
|
76
67
|
// ── Access Gates ─────────────────────────────────────────────────────────
|
|
77
68
|
export { AccessGate, SlotAccessGate } from './access-gate.tsx';
|
|
69
|
+
export type { AccessGateProps, SlotAccessGateProps, CreateElement } from './access-gate.tsx';
|
|
78
70
|
|
|
79
71
|
// ── Status-Code Resolver ─────────────────────────────────────────────────
|
|
80
72
|
export { resolveStatusFile, resolveSlotDenied } from './status-code-resolver.ts';
|
|
@@ -64,9 +64,6 @@ interface MetadataSettleProps {
|
|
|
64
64
|
* children. Its sole job is error ownership: awaiting the metadata
|
|
65
65
|
* promise so that rejections surface at THIS tree position (caught by
|
|
66
66
|
* the owning segment's error boundary, not MetadataHead's).
|
|
67
|
-
*
|
|
68
|
-
* Shell settlement (onSettled) is handled by the collector attaching
|
|
69
|
-
* directly to the promise — unconditional on render progress.
|
|
70
67
|
*/
|
|
71
68
|
async function MetadataSettle(props: MetadataSettleProps): Promise<ReactNode> {
|
|
72
69
|
await props.promise;
|
|
@@ -79,7 +76,6 @@ export interface MetadataCollectorProps {
|
|
|
79
76
|
metadataExport: Metadata | (() => Promise<Metadata>);
|
|
80
77
|
segmentName: string;
|
|
81
78
|
isPage: boolean;
|
|
82
|
-
onSettled?: () => void;
|
|
83
79
|
children: ReactNode;
|
|
84
80
|
}
|
|
85
81
|
|
|
@@ -92,13 +88,11 @@ export interface MetadataCollectorProps {
|
|
|
92
88
|
* Static metadata objects are pushed synchronously (no side effects).
|
|
93
89
|
* Dynamic metadata functions are started (not awaited) and pushed as
|
|
94
90
|
* promises — React descends into children immediately while the I/O
|
|
95
|
-
* runs concurrently (TIM-1367).
|
|
96
|
-
*
|
|
97
|
-
* sibling MetadataSettle component awaits the promise for error
|
|
98
|
-
* ownership only.
|
|
91
|
+
* runs concurrently (TIM-1367). A sibling MetadataSettle component
|
|
92
|
+
* awaits the promise for error ownership.
|
|
99
93
|
*/
|
|
100
94
|
export function MetadataCollector(props: MetadataCollectorProps): ReactNode {
|
|
101
|
-
const { metadataExport, segmentName, isPage,
|
|
95
|
+
const { metadataExport, segmentName, isPage, children } = props;
|
|
102
96
|
|
|
103
97
|
if (typeof metadataExport === 'function') {
|
|
104
98
|
const promise = withSpan('timber.metadata', { 'timber.segment': segmentName }, () =>
|
|
@@ -108,14 +102,6 @@ export function MetadataCollector(props: MetadataCollectorProps): ReactNode {
|
|
|
108
102
|
// both attach handlers, but a fast-rejecting promise can fire
|
|
109
103
|
// Node's unhandledRejection before they do.
|
|
110
104
|
promise.catch(() => {});
|
|
111
|
-
// Shell settlement: unconditional on render progress. If React
|
|
112
|
-
// never reaches MetadataSettle (e.g., parent error boundary fires
|
|
113
|
-
// first), the HTTP status still commits on time.
|
|
114
|
-
if (onSettled)
|
|
115
|
-
promise.then(
|
|
116
|
-
() => onSettled(),
|
|
117
|
-
() => onSettled()
|
|
118
|
-
);
|
|
119
105
|
|
|
120
106
|
getDeferredMetadataEntries().push({ metadata: promise, isPage });
|
|
121
107
|
|
|
@@ -123,7 +109,6 @@ export function MetadataCollector(props: MetadataCollectorProps): ReactNode {
|
|
|
123
109
|
}
|
|
124
110
|
|
|
125
111
|
getDeferredMetadataEntries().push({ metadata: metadataExport, isPage });
|
|
126
|
-
onSettled?.();
|
|
127
112
|
return children;
|
|
128
113
|
}
|
|
129
114
|
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* the revalidation renderer in `rsc-entry/index.ts`) without pulling
|
|
8
8
|
* the entire pipeline phase module along with it.
|
|
9
9
|
*
|
|
10
|
-
* The function throws `ParamCoercionError`
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* The function throws `ParamCoercionError` (defined below) on any codec
|
|
11
|
+
* failure; the pipeline catches that and dispatches to the 404 page.
|
|
12
|
+
* See design/07-routing.md §"Where Coercion Runs".
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { Codec } from '../codec.ts';
|
|
@@ -18,7 +18,16 @@ import { effectiveUrlSegment } from '../routing/segment-classify.ts';
|
|
|
18
18
|
import { toBracketKey } from '../params/resolve-schema.ts';
|
|
19
19
|
import type { RouteMatch } from './pipeline.ts';
|
|
20
20
|
import { normalizeParamValue, type CoercedParams } from '../shared/param-value.ts';
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Thrown when an app/schema.ts segment-param codec's parse() fails.
|
|
23
|
+
* The pipeline catches this and responds with 404.
|
|
24
|
+
*/
|
|
25
|
+
export class ParamCoercionError extends Error {
|
|
26
|
+
constructor(message: string) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = 'ParamCoercionError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
22
31
|
import { isDebug } from './debug.ts';
|
|
23
32
|
|
|
24
33
|
// ---------------------------------------------------------------------------
|
|
@@ -26,15 +26,40 @@ import {
|
|
|
26
26
|
import { markResponseFlushed } from './request-context.ts';
|
|
27
27
|
import { RedirectSignal, DenySignal } from './primitives.ts';
|
|
28
28
|
import { isDebug } from './debug.ts';
|
|
29
|
-
import {
|
|
29
|
+
import { errorResponseHeaders } from './error-response-headers.ts';
|
|
30
|
+
import { rscErrorEnvelope } from './rsc-error-envelope.ts';
|
|
30
31
|
import { isRscRequest } from '../shared/rsc-media-type.ts';
|
|
31
32
|
import type { PipelineConfig, RouteMatch } from './pipeline.ts';
|
|
32
33
|
|
|
33
34
|
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
34
35
|
|
|
35
|
-
function rscErrorResponse(isRsc: boolean, status: number, headers
|
|
36
|
-
if (
|
|
37
|
-
return
|
|
36
|
+
function rscErrorResponse(isRsc: boolean, status: number, headers: Headers): Response {
|
|
37
|
+
if (isRsc) return rscErrorEnvelope(status, headers);
|
|
38
|
+
return new Response(null, { status, headers: errorResponseHeaders(headers) });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the header bag a signal/error outcome is written onto, with the
|
|
43
|
+
* cookie jar applied exactly once.
|
|
44
|
+
*
|
|
45
|
+
* Ownership rule (design/49 §P0b): the phase that owns `responseHeaders`
|
|
46
|
+
* applies the jar to it once; the translator applies it only for outcomes
|
|
47
|
+
* that arrive without it. Render-phase outcomes already carry the jar —
|
|
48
|
+
* `runRenderPhase()` applies it on entry, with or without a middleware
|
|
49
|
+
* chain — so re-applying here emitted every middleware `Set-Cookie` twice
|
|
50
|
+
* (TIM-1491).
|
|
51
|
+
*
|
|
52
|
+
* The guard is "not render", not "only middleware": proxy-phase signals
|
|
53
|
+
* reach the translator with no `responseHeaders` at all, and for a
|
|
54
|
+
* `proxy.ts` that awaits `next()` and then throws, this is the only place
|
|
55
|
+
* the cookies downstream middleware set can reach the wire
|
|
56
|
+
* (design/29-cookies.md §"proxy.ts Cookie Access"). Middleware-phase
|
|
57
|
+
* signals throw before that phase's own application, so they need it too.
|
|
58
|
+
*/
|
|
59
|
+
function outcomeHeaders(outcome: PhaseOutcome, ctx: OutcomeContext): Headers {
|
|
60
|
+
const headers = ctx.responseHeaders ?? new Headers();
|
|
61
|
+
if (outcome.phase !== 'render') applyCookieJar(headers);
|
|
62
|
+
return headers;
|
|
38
63
|
}
|
|
39
64
|
|
|
40
65
|
// ─── Phase Outcome ─────────────────────────────────────────────────────────
|
|
@@ -100,14 +125,12 @@ export async function outcomeToResponse(
|
|
|
100
125
|
}
|
|
101
126
|
|
|
102
127
|
case 'redirect': {
|
|
103
|
-
const headers = ctx
|
|
104
|
-
applyCookieJar(headers);
|
|
128
|
+
const headers = outcomeHeaders(outcome, ctx);
|
|
105
129
|
return buildRedirectResponse(outcome.signal, ctx.req, headers);
|
|
106
130
|
}
|
|
107
131
|
|
|
108
132
|
case 'deny': {
|
|
109
|
-
const headers = ctx
|
|
110
|
-
applyCookieJar(headers);
|
|
133
|
+
const headers = outcomeHeaders(outcome, ctx);
|
|
111
134
|
if (config.renderDenyFallback) {
|
|
112
135
|
try {
|
|
113
136
|
// Clone user-supplied deny-page responses so downstream
|
|
@@ -149,7 +172,7 @@ export async function outcomeToResponse(
|
|
|
149
172
|
await fireOnRequestError(outcome.error, ctx.req, 'proxy');
|
|
150
173
|
if (config.onPipelineError && outcome.error instanceof Error)
|
|
151
174
|
config.onPipelineError(outcome.error, 'proxy');
|
|
152
|
-
return rscErrorResponse(isRsc, 500);
|
|
175
|
+
return rscErrorResponse(isRsc, 500, outcomeHeaders(outcome, ctx));
|
|
153
176
|
}
|
|
154
177
|
|
|
155
178
|
if (outcome.phase === 'middleware') {
|
|
@@ -158,11 +181,10 @@ export async function outcomeToResponse(
|
|
|
158
181
|
if (config.onPipelineError && outcome.error instanceof Error) {
|
|
159
182
|
config.onPipelineError(outcome.error, 'middleware');
|
|
160
183
|
}
|
|
161
|
-
return rscErrorResponse(isRsc, 500);
|
|
184
|
+
return rscErrorResponse(isRsc, 500, outcomeHeaders(outcome, ctx));
|
|
162
185
|
}
|
|
163
186
|
|
|
164
|
-
const headers = ctx
|
|
165
|
-
applyCookieJar(headers);
|
|
187
|
+
const headers = outcomeHeaders(outcome, ctx);
|
|
166
188
|
logRenderError({ method: ctx.method, path: ctx.path, error: outcome.error });
|
|
167
189
|
await fireOnRequestError(outcome.error, ctx.req, 'render');
|
|
168
190
|
if (config.onPipelineError && outcome.error instanceof Error)
|
|
@@ -190,7 +212,7 @@ export async function outcomeToResponse(
|
|
|
190
212
|
config.onPipelineError(fallbackRenderError, 'render');
|
|
191
213
|
}
|
|
192
214
|
}
|
|
193
|
-
return
|
|
215
|
+
return rscErrorResponse(false, 500, headers);
|
|
194
216
|
}
|
|
195
217
|
}
|
|
196
218
|
}
|
|
@@ -25,8 +25,8 @@ import {
|
|
|
25
25
|
import { withSpan } from './tracing.ts';
|
|
26
26
|
import { requestCspNonce } from './csp-nonce.ts';
|
|
27
27
|
import { logRenderError } from './logger.ts';
|
|
28
|
-
import {
|
|
29
|
-
import { ParamCoercionError } from './
|
|
28
|
+
import { isDenySignal, isRedirectSignal } from './primitives.ts';
|
|
29
|
+
import { ParamCoercionError } from './param-coercion.ts';
|
|
30
30
|
import { checkVersionSkew, applyReloadHeaders } from './version-skew.ts';
|
|
31
31
|
import { serveStaticMetadataFile, serializeSitemap } from './pipeline-metadata.ts';
|
|
32
32
|
import { loadModule } from './safe-load.ts';
|
|
@@ -96,10 +96,10 @@ export async function runProxyPhase(
|
|
|
96
96
|
);
|
|
97
97
|
return { kind: 'response', phase: 'proxy', response };
|
|
98
98
|
} catch (error) {
|
|
99
|
-
if (error
|
|
99
|
+
if (isRedirectSignal(error)) {
|
|
100
100
|
return { kind: 'redirect', phase: 'proxy', signal: error };
|
|
101
101
|
}
|
|
102
|
-
if (error
|
|
102
|
+
if (isDenySignal(error)) {
|
|
103
103
|
return { kind: 'deny', phase: 'proxy', signal: error };
|
|
104
104
|
}
|
|
105
105
|
return { kind: 'error', phase: 'proxy', error };
|
|
@@ -169,18 +169,12 @@ export async function runMiddlewarePhase(
|
|
|
169
169
|
// injected request headers so getHeaders() returns them downstream.
|
|
170
170
|
applyRequestHeaderOverlay(requestHeaderOverlay);
|
|
171
171
|
|
|
172
|
-
// Apply cookie jar to response headers before render commits them.
|
|
173
|
-
// This preserves the historical ordering where middleware cookie writes
|
|
174
|
-
// are visible to route-handler header merging, while handler Set-Cookie
|
|
175
|
-
// values still come after middleware cookies and therefore take precedence.
|
|
176
|
-
applyCookieJar(responseHeaders);
|
|
177
|
-
|
|
178
172
|
return runRenderPhase(config, req, match, responseHeaders, requestHeaderOverlay, renderContext);
|
|
179
173
|
} catch (error) {
|
|
180
|
-
if (error
|
|
174
|
+
if (isRedirectSignal(error)) {
|
|
181
175
|
return { kind: 'redirect', phase: 'middleware', signal: error };
|
|
182
176
|
}
|
|
183
|
-
if (error
|
|
177
|
+
if (isDenySignal(error)) {
|
|
184
178
|
return { kind: 'deny', phase: 'middleware', signal: error };
|
|
185
179
|
}
|
|
186
180
|
return { kind: 'error', phase: 'middleware', error };
|
|
@@ -192,6 +186,17 @@ export async function runMiddlewarePhase(
|
|
|
192
186
|
/**
|
|
193
187
|
* Run the render phase. Wraps the configured renderer in a span and a
|
|
194
188
|
* timing scope, and translates thrown signals into outcome variants.
|
|
189
|
+
*
|
|
190
|
+
* The render phase owns `responseHeaders` from here on, so it is the one
|
|
191
|
+
* place the cookie jar is applied to that bag (design/29-cookies.md
|
|
192
|
+
* §"Applied Exactly Once"). Doing it here rather than in the middleware
|
|
193
|
+
* phase means every render-phase outcome — a 200, or a deny / redirect /
|
|
194
|
+
* error thrown from render — carries the jar whether or not a middleware
|
|
195
|
+
* chain ran, and `outcomeToResponse` can skip render-phase outcomes
|
|
196
|
+
* unconditionally (TIM-1491). Application precedes `config.render` so
|
|
197
|
+
* middleware cookie writes are visible to route-handler header merging,
|
|
198
|
+
* while handler `Set-Cookie` values still come after them and take
|
|
199
|
+
* precedence.
|
|
195
200
|
*/
|
|
196
201
|
export async function runRenderPhase(
|
|
197
202
|
config: PipelineConfig,
|
|
@@ -202,6 +207,7 @@ export async function runRenderPhase(
|
|
|
202
207
|
{ canonicalPathname, interception }: RenderContext
|
|
203
208
|
): Promise<PhaseOutcome> {
|
|
204
209
|
const detailed = config.serverTiming === 'detailed';
|
|
210
|
+
applyCookieJar(responseHeaders);
|
|
205
211
|
try {
|
|
206
212
|
const renderFn = () =>
|
|
207
213
|
config.render(req, match, responseHeaders, requestHeaderOverlay, interception);
|
|
@@ -210,10 +216,10 @@ export async function runRenderPhase(
|
|
|
210
216
|
);
|
|
211
217
|
return { kind: 'response', phase: 'render', response };
|
|
212
218
|
} catch (error) {
|
|
213
|
-
if (error
|
|
219
|
+
if (isDenySignal(error)) {
|
|
214
220
|
return { kind: 'deny', phase: 'render', signal: error };
|
|
215
221
|
}
|
|
216
|
-
if (error
|
|
222
|
+
if (isRedirectSignal(error)) {
|
|
217
223
|
return { kind: 'redirect', phase: 'render', signal: error };
|
|
218
224
|
}
|
|
219
225
|
return { kind: 'error', phase: 'render', error };
|
|
@@ -331,13 +337,13 @@ export async function handleRequest(
|
|
|
331
337
|
// calls deny(404) for an unknown tenant, or a dynamic icon that
|
|
332
338
|
// redirects to a CDN-hosted asset) translate to proper HTTP responses
|
|
333
339
|
// instead of being logged as 500s.
|
|
334
|
-
if (error
|
|
340
|
+
if (isRedirectSignal(error)) {
|
|
335
341
|
return new Response(null, {
|
|
336
342
|
status: error.status,
|
|
337
343
|
headers: { Location: error.location },
|
|
338
344
|
});
|
|
339
345
|
}
|
|
340
|
-
if (error
|
|
346
|
+
if (isDenySignal(error)) {
|
|
341
347
|
return new Response(null, { status: error.status });
|
|
342
348
|
}
|
|
343
349
|
logRenderError({ method, path, error });
|