@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
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
type GraphUpdateChannel,
|
|
37
37
|
} from './graph-endpoint.ts';
|
|
38
38
|
import { compressResponse } from '../server/compress.ts';
|
|
39
|
+
import { isSsrStreamError } from '../server/primitives.ts';
|
|
39
40
|
import { isMetadataRouteServePath } from '../server/metadata-routes.ts';
|
|
40
41
|
import { canonicalize } from '../server/canonicalize.ts';
|
|
41
42
|
import { matchUrlParts } from '../server/tree-match.ts';
|
|
@@ -401,7 +402,7 @@ function createTimberMiddleware(server: ViteDevServer, ctx: PluginContext) {
|
|
|
401
402
|
// message; the developer needs to see the actual error (e.g.,
|
|
402
403
|
// "useSegmentParams is not a function"), not the wrapper.
|
|
403
404
|
const displayError =
|
|
404
|
-
error
|
|
405
|
+
isSsrStreamError(error) && error.cause instanceof Error ? error.cause : error;
|
|
405
406
|
const phase = classifyErrorPhase(displayError, projectRoot);
|
|
406
407
|
sendErrorToOverlay(server, displayError, phase, projectRoot);
|
|
407
408
|
respond500(res, displayError, phase, projectRoot, hmrOptions);
|
|
@@ -60,6 +60,8 @@ const TIMBER_ENV_DTS = [
|
|
|
60
60
|
'/* eslint-disable @typescript-eslint/triple-slash-reference */',
|
|
61
61
|
'/// <reference path=".timber/timber-routes.d.ts" />',
|
|
62
62
|
'/// <reference types="vite/client" />',
|
|
63
|
+
'/// <reference types="react/canary" />',
|
|
64
|
+
'/// <reference types="react-dom/canary" />',
|
|
63
65
|
'',
|
|
64
66
|
].join('\n');
|
|
65
67
|
|
|
@@ -109,8 +109,8 @@ export interface SlotInterceptionRewrite extends InterceptionRewriteBase {
|
|
|
109
109
|
* either. The server resolves the owner by `interceptingScope` — a tree path,
|
|
110
110
|
* which identifies exactly one directory — and builds the rendered chain as
|
|
111
111
|
* `[…owner ancestors, interceptingNode, …its matched descendants]`. That is an
|
|
112
|
-
* ordinary segment chain, so `
|
|
113
|
-
* that interception happened.
|
|
112
|
+
* ordinary segment chain, so `route-element-builder.ts` renders it with no
|
|
113
|
+
* knowledge that interception happened.
|
|
114
114
|
*
|
|
115
115
|
* Resolving the owner from the tree rather than from a URL is what makes this
|
|
116
116
|
* shape work where the modal shape cannot: `interceptingPrefix` is
|
|
@@ -15,8 +15,8 @@ import type { SegmentNode } from './types.ts';
|
|
|
15
15
|
* Reject every parallel slot that no renderer reaches.
|
|
16
16
|
*
|
|
17
17
|
* **Where slots are resolved.** Two places read `segment.slots`, and both walk
|
|
18
|
-
* the main route chain: `route-element-builder.ts` (the RSC render)
|
|
19
|
-
*
|
|
18
|
+
* the main route chain: `route-element-builder.ts` (the RSC render).
|
|
19
|
+
* It is not reachable from inside a slot. The slot's own
|
|
20
20
|
* subtree is rendered by `slot-resolver.ts`, which walks the slot's matched
|
|
21
21
|
* chain to apply that chain's layouts, error boundaries and access gates
|
|
22
22
|
* (`wrapWithIntermediateSegments`, `wrapWithLayout`) and never reads `.slots`
|
|
@@ -13,13 +13,93 @@
|
|
|
13
13
|
* See design/04-authorization.md and design/02-rendering-pipeline.md §"AccessGate"
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { DenySignal, RedirectSignal } from './primitives.ts';
|
|
17
|
-
import type { AccessGateProps, SlotAccessGateProps } from './tree-builder.ts';
|
|
16
|
+
import { isDenySignal, isRedirectSignal, DenySignal, RedirectSignal } from './primitives.ts';
|
|
18
17
|
import { withSpan, setSpanAttribute } from './tracing.ts';
|
|
19
18
|
import { isDebug } from './debug.ts';
|
|
20
19
|
import type { DenyPageEntry } from './deny-boundary.ts';
|
|
21
20
|
import { handleCaughtDeny, setFirstDeniedSegmentIndex } from './deny-boundary.ts';
|
|
22
21
|
import type { ReactNode } from 'react';
|
|
22
|
+
import type { LoadedComponent } from './utils/element-type.ts';
|
|
23
|
+
|
|
24
|
+
// ─── Types ─────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Function that creates a React element. Matches React.createElement signature.
|
|
28
|
+
*
|
|
29
|
+
* `props` is typed as `object | null` rather than `Record<string, unknown>` so
|
|
30
|
+
* that interface types with known keys (e.g. `AccessGateProps`,
|
|
31
|
+
* `ErrorBoundaryProps`) flow through without an explicit index-signature cast.
|
|
32
|
+
*/
|
|
33
|
+
export type CreateElement = (
|
|
34
|
+
type: unknown,
|
|
35
|
+
props: object | null,
|
|
36
|
+
...children: unknown[]
|
|
37
|
+
) => ReactNode;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Framework-injected access gate component.
|
|
41
|
+
*
|
|
42
|
+
* When `verdict` is provided (from the pre-render pass), AccessGate replays
|
|
43
|
+
* the stored result synchronously — no re-execution, no async, immune to
|
|
44
|
+
* Suspense timing. When `verdict` is absent, falls back to calling `accessFn`
|
|
45
|
+
* (backward compat for construction sites that don't run a pre-render pass).
|
|
46
|
+
*/
|
|
47
|
+
export interface AccessGateProps {
|
|
48
|
+
accessFn: () => unknown;
|
|
49
|
+
/** Segment name for dev logging (e.g. "authenticated", "dashboard"). */
|
|
50
|
+
segmentName?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Segment key of the gated segment (`computeSegmentKeys`). Decides whether
|
|
53
|
+
* a matched deny page renders here or is hoisted to the segment that owns
|
|
54
|
+
* it. Omitted by construction sites that build no deny chains — with no
|
|
55
|
+
* chain there is nothing to place. See TIM-1356.
|
|
56
|
+
*/
|
|
57
|
+
segmentKey?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Index of this segment in the matched segment chain. Recorded in ALS
|
|
60
|
+
* when the gate catches a DenySignal so post-render consumers
|
|
61
|
+
* (buildSegmentInfo, X-Timber-Segments) can bound the segment list
|
|
62
|
+
* without a pre-render access pass. See TIM-1363.
|
|
63
|
+
*/
|
|
64
|
+
segmentIndex?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Deny page fallback chain. When provided and a DenySignal is caught,
|
|
67
|
+
* AccessGate renders the matching deny page in-tree instead of throwing.
|
|
68
|
+
* This prevents the error from reaching React Flight, eliminating the
|
|
69
|
+
* second render pass. See TIM-666.
|
|
70
|
+
*
|
|
71
|
+
* Omitted for skipped segments — a deny there must hard-navigate to
|
|
72
|
+
* avoid rendering the deny page inside cached layout chrome (TIM-1074).
|
|
73
|
+
*/
|
|
74
|
+
denyPages?: DenyPageEntry[];
|
|
75
|
+
/**
|
|
76
|
+
* When true, this gate wraps a skipped segment. On deny, the gate
|
|
77
|
+
* records the deny in ALS and rethrows so the response path can
|
|
78
|
+
* return rscErrorEnvelope for a client hard-navigate.
|
|
79
|
+
*/
|
|
80
|
+
skippedSegment?: boolean;
|
|
81
|
+
children: ReactNode;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Framework-injected slot access gate component.
|
|
86
|
+
* On denial, renders denied.tsx → default.tsx → null instead of failing the page.
|
|
87
|
+
*
|
|
88
|
+
* DeniedComponent is passed instead of a pre-built element so that
|
|
89
|
+
* SlotAccessGate can forward DenySignal.data as dangerouslyPassData
|
|
90
|
+
* and slotName as the slot prop after catching the signal.
|
|
91
|
+
*/
|
|
92
|
+
export interface SlotAccessGateProps {
|
|
93
|
+
accessFn: () => unknown;
|
|
94
|
+
/** The denied.tsx component (not a pre-built element). null if no denied.tsx exists. */
|
|
95
|
+
DeniedComponent: LoadedComponent | null;
|
|
96
|
+
/** Slot directory name without @ prefix (e.g. "admin", "sidebar"). */
|
|
97
|
+
slotName: string;
|
|
98
|
+
/** createElement function for building elements dynamically. */
|
|
99
|
+
createElement: CreateElement;
|
|
100
|
+
defaultFallback: ReactNode;
|
|
101
|
+
children: ReactNode;
|
|
102
|
+
}
|
|
23
103
|
|
|
24
104
|
// ─── AccessGate ─────────────────────────────────────────────────────────────
|
|
25
105
|
|
|
@@ -49,16 +129,8 @@ import type { ReactNode } from 'react';
|
|
|
49
129
|
* can return rscErrorEnvelope for a client hard-navigate.
|
|
50
130
|
*/
|
|
51
131
|
export function AccessGate(props: AccessGateProps): Promise<ReactNode> {
|
|
52
|
-
const {
|
|
53
|
-
|
|
54
|
-
segmentName,
|
|
55
|
-
segmentKey,
|
|
56
|
-
segmentIndex,
|
|
57
|
-
denyPages,
|
|
58
|
-
skippedSegment,
|
|
59
|
-
onSettled,
|
|
60
|
-
children,
|
|
61
|
-
} = props;
|
|
132
|
+
const { accessFn, segmentName, segmentKey, segmentIndex, denyPages, skippedSegment, children } =
|
|
133
|
+
props;
|
|
62
134
|
|
|
63
135
|
return accessGateFallback(
|
|
64
136
|
accessFn,
|
|
@@ -67,7 +139,6 @@ export function AccessGate(props: AccessGateProps): Promise<ReactNode> {
|
|
|
67
139
|
segmentIndex,
|
|
68
140
|
skippedSegment ? undefined : denyPages,
|
|
69
141
|
skippedSegment,
|
|
70
|
-
onSettled,
|
|
71
142
|
children
|
|
72
143
|
);
|
|
73
144
|
}
|
|
@@ -83,7 +154,6 @@ async function accessGateFallback(
|
|
|
83
154
|
segmentIndex: AccessGateProps['segmentIndex'],
|
|
84
155
|
denyPages: DenyPageEntry[] | undefined,
|
|
85
156
|
skippedSegment: boolean | undefined,
|
|
86
|
-
onSettled: (() => void) | undefined,
|
|
87
157
|
children: ReactNode
|
|
88
158
|
): Promise<ReactNode> {
|
|
89
159
|
try {
|
|
@@ -92,20 +162,20 @@ async function accessGateFallback(
|
|
|
92
162
|
await accessFn();
|
|
93
163
|
await setSpanAttribute('timber.result', 'pass');
|
|
94
164
|
} catch (error: unknown) {
|
|
95
|
-
if (error
|
|
165
|
+
if (isDenySignal(error)) {
|
|
96
166
|
await setSpanAttribute('timber.result', 'deny');
|
|
97
167
|
await setSpanAttribute('timber.deny_status', error.status);
|
|
98
168
|
if (error.sourceFile) {
|
|
99
169
|
await setSpanAttribute('timber.deny_file', error.sourceFile);
|
|
100
170
|
}
|
|
101
|
-
} else if (error
|
|
171
|
+
} else if (isRedirectSignal(error)) {
|
|
102
172
|
await setSpanAttribute('timber.result', 'redirect');
|
|
103
173
|
}
|
|
104
174
|
throw error;
|
|
105
175
|
}
|
|
106
176
|
});
|
|
107
177
|
} catch (error: unknown) {
|
|
108
|
-
if (error
|
|
178
|
+
if (isDenySignal(error)) {
|
|
109
179
|
if (segmentIndex !== undefined) {
|
|
110
180
|
setFirstDeniedSegmentIndex(segmentIndex);
|
|
111
181
|
}
|
|
@@ -120,8 +190,6 @@ async function accessGateFallback(
|
|
|
120
190
|
}
|
|
121
191
|
}
|
|
122
192
|
throw error;
|
|
123
|
-
} finally {
|
|
124
|
-
onSettled?.();
|
|
125
193
|
}
|
|
126
194
|
|
|
127
195
|
return children;
|
|
@@ -151,7 +219,7 @@ export async function SlotAccessGate(props: SlotAccessGateProps): Promise<ReactN
|
|
|
151
219
|
} catch (error: unknown) {
|
|
152
220
|
// DenySignal → graceful degradation (denied.tsx → default.tsx → null)
|
|
153
221
|
// Build the denied element dynamically so DenySignal.data is forwarded.
|
|
154
|
-
if (error
|
|
222
|
+
if (isDenySignal(error)) {
|
|
155
223
|
return (
|
|
156
224
|
buildDeniedFallback(DeniedComponent, slotName, error.data, createElement) ??
|
|
157
225
|
defaultFallback ??
|
|
@@ -163,7 +231,7 @@ export async function SlotAccessGate(props: SlotAccessGateProps): Promise<ReactN
|
|
|
163
231
|
// Slot access should use deny(), not redirect(). Redirecting from a
|
|
164
232
|
// slot would redirect the entire page, which breaks the contract that
|
|
165
233
|
// slot failure is graceful degradation.
|
|
166
|
-
if (error
|
|
234
|
+
if (isRedirectSignal(error)) {
|
|
167
235
|
if (isDebug()) {
|
|
168
236
|
console.error(
|
|
169
237
|
'[timber] redirect() is not allowed in slot access.ts. ' +
|
|
@@ -223,7 +223,7 @@ async function runActionMiddleware<TCtx>(
|
|
|
223
223
|
import { parseFormData } from './form-data.ts';
|
|
224
224
|
import { formatSize } from '../utils/format.ts';
|
|
225
225
|
import { isDebug, isDevMode } from './debug.ts';
|
|
226
|
-
import {
|
|
226
|
+
import { isDenySignal, isRedirectSignal } from './primitives.ts';
|
|
227
227
|
import {
|
|
228
228
|
stripSensitiveFields,
|
|
229
229
|
resolveSensitivePredicate,
|
|
@@ -432,7 +432,7 @@ export function createActionClient<TCtx = Record<string, never>>(
|
|
|
432
432
|
// They must propagate to executeAction() which converts them to proper
|
|
433
433
|
// HTTP responses (302 redirect, 4xx deny). Catching them here would
|
|
434
434
|
// wrap them as INTERNAL_ERROR and break redirect()/redirectExternal()/deny().
|
|
435
|
-
if (error
|
|
435
|
+
if (isRedirectSignal(error) || isDenySignal(error)) {
|
|
436
436
|
throw error;
|
|
437
437
|
}
|
|
438
438
|
return handleActionError(error);
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
} from '../rsc-runtime/rsc.ts';
|
|
22
22
|
|
|
23
23
|
import { validateCsrf, type CsrfConfig } from './csrf.ts';
|
|
24
|
-
import {
|
|
24
|
+
import { isDenySignal, isRedirectSignal } from './primitives.ts';
|
|
25
25
|
import { executeAction, type RevalidateRenderer } from './actions.ts';
|
|
26
26
|
import { runWithRequestContext, setMutableCookieContext } from './request-context.ts';
|
|
27
27
|
import { getSetCookieHeaders, getCookiesForSsr } from './cookie-context.ts';
|
|
@@ -38,6 +38,7 @@ import type { FormFlashData } from './form-flash.ts';
|
|
|
38
38
|
import { checkVersionSkew, applyReloadHeaders } from './version-skew.ts';
|
|
39
39
|
import { logActionError } from './logger.ts';
|
|
40
40
|
import { withTimeout, RenderTimeoutError } from './render-timeout.ts';
|
|
41
|
+
import { canonicalize } from './canonicalize.ts';
|
|
41
42
|
import { RSC_CONTENT_TYPE } from '../shared/rsc-media-type.ts';
|
|
42
43
|
|
|
43
44
|
// ─── Types ────────────────────────────────────────────────────────────────
|
|
@@ -246,8 +247,14 @@ async function handleRscAction(
|
|
|
246
247
|
// of leaking stack traces as 500s.
|
|
247
248
|
let result;
|
|
248
249
|
try {
|
|
250
|
+
const requestUrl = new URL(req.url);
|
|
251
|
+
// Canonicalize the request pathname so it matches the decoded form
|
|
252
|
+
// that revalidationPathMatches uses on the target side.
|
|
253
|
+
const canonical = canonicalize(requestUrl.pathname, true);
|
|
249
254
|
result = await executeAction(actionFn, args, {
|
|
250
255
|
renderer: config.revalidateRenderer,
|
|
256
|
+
requestPathname: canonical.ok ? canonical.pathname : requestUrl.pathname,
|
|
257
|
+
requestSearch: requestUrl.search,
|
|
251
258
|
});
|
|
252
259
|
} catch (error) {
|
|
253
260
|
// Log full error server-side for debugging
|
|
@@ -272,13 +279,14 @@ async function handleRscAction(
|
|
|
272
279
|
_status: result.redirectStatus ?? 302,
|
|
273
280
|
};
|
|
274
281
|
const rscStream = renderToReadableStream(redirectPayload);
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
+
const redirectHeaders: Record<string, string> = {
|
|
283
|
+
'Content-Type': RSC_CONTENT_TYPE,
|
|
284
|
+
'X-Timber-Redirect': result.redirectTo,
|
|
285
|
+
};
|
|
286
|
+
if (result.invalidatedPaths && result.invalidatedPaths.length > 0) {
|
|
287
|
+
redirectHeaders['X-Timber-Revalidation-Path'] = result.invalidatedPaths.join(' ');
|
|
288
|
+
}
|
|
289
|
+
return new Response(rscStream, { status: 200, headers: redirectHeaders });
|
|
282
290
|
}
|
|
283
291
|
|
|
284
292
|
// Render the action result as an RSC stream.
|
|
@@ -289,6 +297,11 @@ async function handleRscAction(
|
|
|
289
297
|
'Content-Type': RSC_CONTENT_TYPE,
|
|
290
298
|
};
|
|
291
299
|
|
|
300
|
+
// TIM-1454: Tell the client which paths were invalidated (non-matching).
|
|
301
|
+
if (result.invalidatedPaths && result.invalidatedPaths.length > 0) {
|
|
302
|
+
headers['X-Timber-Revalidation-Path'] = result.invalidatedPaths.join(' ');
|
|
303
|
+
}
|
|
304
|
+
|
|
292
305
|
if (result.revalidation) {
|
|
293
306
|
// Wrapper object — Next.js-style pattern: action result + element tree
|
|
294
307
|
// serialized together so React Flight handles both in one stream.
|
|
@@ -313,9 +326,9 @@ async function handleRscAction(
|
|
|
313
326
|
signal: req.signal,
|
|
314
327
|
onError(error: unknown) {
|
|
315
328
|
// ??= keeps the first signal (shallowest AccessGate in top-down render).
|
|
316
|
-
if (error
|
|
329
|
+
if (isDenySignal(error)) {
|
|
317
330
|
serializationSignal ??= { kind: 'deny', status: error.status };
|
|
318
|
-
} else if (error
|
|
331
|
+
} else if (isRedirectSignal(error)) {
|
|
319
332
|
serializationSignal ??= {
|
|
320
333
|
kind: 'redirect',
|
|
321
334
|
location: error.location,
|
package/src/server/actions.ts
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { cache } from '../cache/cache-api.ts';
|
|
18
|
-
import {
|
|
18
|
+
import { canonicalize } from './canonicalize.ts';
|
|
19
|
+
import { isDenySignal, isRedirectSignal } from './primitives.ts';
|
|
19
20
|
import { withSpan } from './tracing.ts';
|
|
20
21
|
import { revalidationAls, type RevalidationState } from './als-registry.ts';
|
|
21
22
|
// ─── Types ───────────────────────────────────────────────────────────────
|
|
@@ -24,7 +25,7 @@ import { revalidationAls, type RevalidationState } from './als-registry.ts';
|
|
|
24
25
|
export interface RevalidationResult {
|
|
25
26
|
/**
|
|
26
27
|
* The payload root — tree plus the params it rendered with, exactly as an
|
|
27
|
-
* ordinary route payload. The client splits it in `
|
|
28
|
+
* ordinary route payload. The client splits it in `applyActionResult`.
|
|
28
29
|
*/
|
|
29
30
|
payload: unknown;
|
|
30
31
|
}
|
|
@@ -39,6 +40,15 @@ export type { RevalidationState } from './als-registry.ts';
|
|
|
39
40
|
export interface ActionHandlerConfig {
|
|
40
41
|
/** Renderer for producing RSC payloads during revalidation. */
|
|
41
42
|
renderer?: RevalidateRenderer;
|
|
43
|
+
/**
|
|
44
|
+
* Pathname of the page the action was submitted from. When set,
|
|
45
|
+
* revalidatePath renders are skipped for non-matching paths — the
|
|
46
|
+
* server cache is already invalidated, so the next navigation fetches
|
|
47
|
+
* fresh data. See TIM-1454.
|
|
48
|
+
*/
|
|
49
|
+
requestPathname?: string;
|
|
50
|
+
/** Search string of the action request URL (e.g. `?page=1`). */
|
|
51
|
+
requestSearch?: string;
|
|
42
52
|
}
|
|
43
53
|
|
|
44
54
|
/** Result of handling a server action request. */
|
|
@@ -47,6 +57,12 @@ export interface ActionHandlerResult {
|
|
|
47
57
|
actionResult: unknown;
|
|
48
58
|
/** Revalidation result if revalidatePath was called (element tree, not yet serialized). */
|
|
49
59
|
revalidation?: RevalidationResult;
|
|
60
|
+
/**
|
|
61
|
+
* Paths passed to `revalidatePath()` that don't match the current page
|
|
62
|
+
* (TIM-1454). The client invalidates its caches for these so the next
|
|
63
|
+
* navigation fetches fresh data.
|
|
64
|
+
*/
|
|
65
|
+
invalidatedPaths?: string[];
|
|
50
66
|
/** Redirect location if a RedirectSignal was thrown during revalidation. */
|
|
51
67
|
redirectTo?: string;
|
|
52
68
|
/** Redirect status code. */
|
|
@@ -112,6 +128,13 @@ export function revalidateTag(tag: string): void {
|
|
|
112
128
|
}
|
|
113
129
|
}
|
|
114
130
|
|
|
131
|
+
// ─── Helpers ────────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
function stripFragment(path: string): string {
|
|
134
|
+
const idx = path.indexOf('#');
|
|
135
|
+
return idx >= 0 ? path.slice(0, idx) : path;
|
|
136
|
+
}
|
|
137
|
+
|
|
115
138
|
// ─── Action Handler ──────────────────────────────────────────────────────
|
|
116
139
|
|
|
117
140
|
/**
|
|
@@ -151,7 +174,7 @@ export async function executeAction(
|
|
|
151
174
|
() => actionFn(...args)
|
|
152
175
|
);
|
|
153
176
|
} catch (error) {
|
|
154
|
-
if (error
|
|
177
|
+
if (isRedirectSignal(error)) {
|
|
155
178
|
redirectTo = error.location;
|
|
156
179
|
redirectStatus = error.status;
|
|
157
180
|
} else {
|
|
@@ -182,39 +205,36 @@ export async function executeAction(
|
|
|
182
205
|
}
|
|
183
206
|
}
|
|
184
207
|
|
|
185
|
-
// Process path revalidation — build element tree (not yet serialized)
|
|
208
|
+
// Process path revalidation — build element tree (not yet serialized).
|
|
209
|
+
//
|
|
210
|
+
// TIM-1454: Only render the path if it matches the current page. Non-matching
|
|
211
|
+
// paths are collected as `invalidatedPaths` — the client evicts them from its
|
|
212
|
+
// caches so the next navigation fetches fresh data.
|
|
186
213
|
let revalidation: RevalidationResult | undefined;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
214
|
+
let invalidatedPaths: string[] | undefined;
|
|
215
|
+
|
|
216
|
+
if (state.paths.length > 0) {
|
|
190
217
|
const path = state.paths[0];
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
`[timber] revalidatePath dropped — target middleware denied (status ${renderError.status})`
|
|
212
|
-
);
|
|
213
|
-
} else {
|
|
214
|
-
// Log but don't fail the action — revalidation is best-effort.
|
|
215
|
-
// Includes input-validation rejections from the renderer
|
|
216
|
-
// (e.g., revalidatePath('//evil.com/admin')).
|
|
217
|
-
console.error('[timber] revalidatePath render failed:', renderError);
|
|
218
|
+
|
|
219
|
+
if (
|
|
220
|
+
config.requestPathname &&
|
|
221
|
+
!revalidationPathMatches(path, config.requestPathname, config.requestSearch)
|
|
222
|
+
) {
|
|
223
|
+
invalidatedPaths = [stripFragment(path)];
|
|
224
|
+
} else if (config.renderer) {
|
|
225
|
+
try {
|
|
226
|
+
revalidation = await config.renderer(path);
|
|
227
|
+
} catch (renderError) {
|
|
228
|
+
if (isRedirectSignal(renderError)) {
|
|
229
|
+
redirectTo = renderError.location;
|
|
230
|
+
redirectStatus = renderError.status;
|
|
231
|
+
} else if (isDenySignal(renderError)) {
|
|
232
|
+
console.error(
|
|
233
|
+
`[timber] revalidatePath dropped — target middleware denied (status ${renderError.status})`
|
|
234
|
+
);
|
|
235
|
+
} else {
|
|
236
|
+
console.error('[timber] revalidatePath render failed:', renderError);
|
|
237
|
+
}
|
|
218
238
|
}
|
|
219
239
|
}
|
|
220
240
|
}
|
|
@@ -222,6 +242,33 @@ export async function executeAction(
|
|
|
222
242
|
return {
|
|
223
243
|
actionResult,
|
|
224
244
|
revalidation,
|
|
245
|
+
invalidatedPaths,
|
|
225
246
|
...(redirectTo ? { redirectTo, redirectStatus } : {}),
|
|
226
247
|
};
|
|
227
248
|
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Check if a `revalidatePath(path)` argument matches the request pathname.
|
|
252
|
+
/**
|
|
253
|
+
* Canonicalizes the revalidated path the same way the renderer does so
|
|
254
|
+
* equivalent paths compare equal. Returns true when the paths match or
|
|
255
|
+
* when comparison is inconclusive (validation failure → render to be safe).
|
|
256
|
+
*/
|
|
257
|
+
function revalidationPathMatches(
|
|
258
|
+
revalidatedPath: string,
|
|
259
|
+
requestPathname: string,
|
|
260
|
+
requestSearch?: string
|
|
261
|
+
): boolean {
|
|
262
|
+
const hashIdx = revalidatedPath.indexOf('#');
|
|
263
|
+
const noFragment = hashIdx >= 0 ? revalidatedPath.slice(0, hashIdx) : revalidatedPath;
|
|
264
|
+
const queryIdx = noFragment.indexOf('?');
|
|
265
|
+
const pathnameOnly = queryIdx >= 0 ? noFragment.slice(0, queryIdx) : noFragment;
|
|
266
|
+
const search = queryIdx >= 0 ? noFragment.slice(queryIdx) : '';
|
|
267
|
+
const canonical = canonicalize(pathnameOnly, true);
|
|
268
|
+
if (!canonical.ok) return true;
|
|
269
|
+
if (canonical.pathname !== requestPathname) return false;
|
|
270
|
+
// revalidatePath('/dashboard') (no search) matches any query variant.
|
|
271
|
+
// revalidatePath('/dashboard?tab=x') only matches that exact search.
|
|
272
|
+
if (search && search !== (requestSearch ?? '')) return false;
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
@@ -131,17 +131,11 @@ export interface RequestContextStore {
|
|
|
131
131
|
/**
|
|
132
132
|
* Set by AccessGate or PageDenyBoundary when a DenySignal is caught
|
|
133
133
|
* server-side (inside the React tree, before React Flight sees it).
|
|
134
|
-
* The
|
|
135
|
-
*
|
|
134
|
+
* The document path reads this after render to set the HTTP status code;
|
|
135
|
+
* the RSC payload path does not read it (a Flight payload response is always 200).
|
|
136
|
+
* See TIM-666, TIM-1495.
|
|
136
137
|
*/
|
|
137
138
|
denyStatus?: number;
|
|
138
|
-
/**
|
|
139
|
-
* Callback fired when setDenyStatus() is called. Used by shellSettled
|
|
140
|
-
* to short-circuit when any component catches a DenySignal in-tree,
|
|
141
|
-
* regardless of which component caught it (AccessGate, TracedLayout,
|
|
142
|
-
* or PageDenyBoundary). See TIM-1208.
|
|
143
|
-
*/
|
|
144
|
-
onDenyStatus?: () => void;
|
|
145
139
|
/**
|
|
146
140
|
* Dev-only: getter for the current request's RSC debug components.
|
|
147
141
|
* Set by renderRoute() so onPipelineError can include component tree
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*
|
|
22
22
|
* The result is an ordinary `RouteMatch` whose chain runs
|
|
23
23
|
* `[…owner ancestors, interceptingNode, …its matched descendants]`. Every
|
|
24
|
-
* consumer downstream — `
|
|
24
|
+
* consumer downstream — `route-element-builder.ts`, access gates,
|
|
25
25
|
* middleware — sees a segment chain and needs to know nothing about
|
|
26
26
|
* interception. That is why this feature adds no rendering path: the chain
|
|
27
27
|
* *is* the feature.
|
|
@@ -18,57 +18,17 @@
|
|
|
18
18
|
* error message, no stack — see design/13-security.md principle 4.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
401: 'Unauthorized',
|
|
29
|
-
402: 'Payment Required',
|
|
30
|
-
403: 'Forbidden',
|
|
31
|
-
404: 'Not Found',
|
|
32
|
-
405: 'Method Not Allowed',
|
|
33
|
-
406: 'Not Acceptable',
|
|
34
|
-
408: 'Request Timeout',
|
|
35
|
-
409: 'Conflict',
|
|
36
|
-
410: 'Gone',
|
|
37
|
-
413: 'Content Too Large',
|
|
38
|
-
415: 'Unsupported Media Type',
|
|
39
|
-
418: "I'm a Teapot",
|
|
40
|
-
422: 'Unprocessable Content',
|
|
41
|
-
429: 'Too Many Requests',
|
|
42
|
-
451: 'Unavailable For Legal Reasons',
|
|
43
|
-
500: 'Internal Server Error',
|
|
44
|
-
501: 'Not Implemented',
|
|
45
|
-
502: 'Bad Gateway',
|
|
46
|
-
503: 'Service Unavailable',
|
|
47
|
-
504: 'Gateway Timeout',
|
|
48
|
-
};
|
|
21
|
+
import { displayStatus, statusReasonPhrase } from '../shared/status-reason-phrase.ts';
|
|
22
|
+
|
|
23
|
+
// The phrase table and status clamp live in `shared/status-reason-phrase.ts`:
|
|
24
|
+
// the client-form terminal (`client/deny-last-resort.tsx`) renders the same
|
|
25
|
+
// words for a deny that streamed past every boundary, and one table keeps the
|
|
26
|
+
// two surfaces from drifting.
|
|
27
|
+
export { statusReasonPhrase };
|
|
49
28
|
|
|
50
29
|
/** HTML content type used for every framework default page response. */
|
|
51
30
|
export const DEFAULT_STATUS_PAGE_CONTENT_TYPE = 'text/html; charset=utf-8';
|
|
52
31
|
|
|
53
|
-
/**
|
|
54
|
-
* Coerce a status into something safe to put in markup.
|
|
55
|
-
*
|
|
56
|
-
* `DenySignal.status` is typed `number` but nothing validates it at
|
|
57
|
-
* construction, and JS callers can hand us anything. Rather than escaping a
|
|
58
|
-
* value that should never have been a string, collapse non-integers to 500 —
|
|
59
|
-
* the page is decoration for a status already on the wire.
|
|
60
|
-
*/
|
|
61
|
-
function displayStatus(status: number): number {
|
|
62
|
-
return Number.isInteger(status) && status >= 100 && status <= 599 ? status : 500;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Human-readable phrase for a status code. Never caller-supplied text. */
|
|
66
|
-
export function statusReasonPhrase(status: number): string {
|
|
67
|
-
const code = displayStatus(status);
|
|
68
|
-
if (REASON_PHRASES[code]) return REASON_PHRASES[code];
|
|
69
|
-
return code >= 500 ? 'Server Error' : 'Request Error';
|
|
70
|
-
}
|
|
71
|
-
|
|
72
32
|
/**
|
|
73
33
|
* Build the framework default page for a status code as a full HTML document.
|
|
74
34
|
*
|