@rsc-kit/core 0.16.3 → 0.18.0
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/barrelImports.d.ts +6 -0
- package/dist/barrelImports.js +93 -0
- package/dist/barrelImports.js.map +1 -0
- package/dist/buildReport.d.ts +6 -1
- package/dist/buildReport.js +16 -18
- package/dist/buildReport.js.map +1 -1
- package/dist/cache.js +8 -4
- package/dist/cache.js.map +1 -1
- package/dist/clientEntries.d.ts +32 -0
- package/dist/clientEntries.js +107 -0
- package/dist/clientEntries.js.map +1 -0
- package/dist/clientImports.d.ts +36 -0
- package/dist/clientImports.js +36 -0
- package/dist/clientImports.js.map +1 -0
- package/dist/devReload.d.ts +19 -0
- package/dist/devReload.js +32 -0
- package/dist/devReload.js.map +1 -0
- package/dist/events.d.ts +57 -0
- package/dist/events.js +113 -0
- package/dist/events.js.map +1 -0
- package/dist/host.d.ts +16 -6
- package/dist/host.js +219 -124
- package/dist/host.js.map +1 -1
- package/dist/hostCalls.d.ts +27 -0
- package/dist/hostCalls.js +127 -12
- package/dist/hostCalls.js.map +1 -1
- package/dist/hostRouting.d.ts +53 -0
- package/dist/hostRouting.js +100 -0
- package/dist/hostRouting.js.map +1 -0
- package/dist/js/DefaultRouteError.d.ts +3 -0
- package/dist/js/DefaultRouteError.js +61 -0
- package/dist/js/DefaultRouteError.js.map +1 -0
- package/dist/js/Form.js +54 -6
- package/dist/js/Form.js.map +1 -1
- package/dist/js/Link.d.ts +0 -3
- package/dist/js/Link.js +3 -6
- package/dist/js/Link.js.map +1 -1
- package/dist/js/SegmentBoundary.d.ts +38 -2
- package/dist/js/SegmentBoundary.js +51 -13
- package/dist/js/SegmentBoundary.js.map +1 -1
- package/dist/js/createViteRscApp.js +32 -17
- package/dist/js/createViteRscApp.js.map +1 -1
- package/dist/js/devNotice.d.ts +2 -2
- package/dist/js/devNotice.js +17 -7
- package/dist/js/devNotice.js.map +1 -1
- package/dist/js/fallbackReport.d.ts +2 -0
- package/dist/js/fallbackReport.js +51 -0
- package/dist/js/fallbackReport.js.map +1 -0
- package/dist/js/navigate.d.ts +1 -1
- package/dist/js/navigate.js +55 -23
- package/dist/js/navigate.js.map +1 -1
- package/dist/js/queryClient.d.ts +2 -1
- package/dist/js/queryClient.js +0 -12
- package/dist/js/queryClient.js.map +1 -1
- package/dist/js/router.d.ts +2 -2
- package/dist/js/router.js.map +1 -1
- package/dist/js/segmentStore.d.ts +2 -0
- package/dist/js/segmentStore.js +21 -1
- package/dist/js/segmentStore.js.map +1 -1
- package/dist/js/staleAssets.js +10 -1
- package/dist/js/staleAssets.js.map +1 -1
- package/dist/js/standardSchema.d.ts +6 -0
- package/dist/js/standardSchema.js +19 -1
- package/dist/js/standardSchema.js.map +1 -1
- package/dist/js/useEvents.d.ts +25 -0
- package/dist/js/useEvents.js +78 -0
- package/dist/js/useEvents.js.map +1 -0
- package/dist/js/useLinkStatus.d.ts +14 -1
- package/dist/js/useLinkStatus.js +15 -1
- package/dist/js/useLinkStatus.js.map +1 -1
- package/dist/js/usePolling.d.ts +29 -0
- package/dist/js/usePolling.js +141 -0
- package/dist/js/usePolling.js.map +1 -0
- package/dist/manifest.d.ts +18 -1
- package/dist/manifest.js.map +1 -1
- package/dist/metadata.d.ts +94 -15
- package/dist/metadata.js.map +1 -1
- package/dist/metadataRoutes.d.ts +61 -0
- package/dist/metadataRoutes.js +173 -0
- package/dist/metadataRoutes.js.map +1 -0
- package/dist/prerender.js +44 -10
- package/dist/prerender.js.map +1 -1
- package/dist/reactCache.d.ts +1 -0
- package/dist/reactCache.js +52 -0
- package/dist/reactCache.js.map +1 -0
- package/dist/redirect.d.ts +16 -2
- package/dist/redirect.js +10 -19
- package/dist/redirect.js.map +1 -1
- package/dist/request.d.ts +18 -1
- package/dist/request.js +91 -5
- package/dist/request.js.map +1 -1
- package/dist/revalidate.d.ts +6 -4
- package/dist/revalidate.js +6 -5
- package/dist/revalidate.js.map +1 -1
- package/dist/routes.d.ts +16 -3
- package/dist/routes.js +5 -5
- package/dist/routes.js.map +1 -1
- package/dist/routing.d.ts +2 -2
- package/dist/routing.js +47 -25
- package/dist/routing.js.map +1 -1
- package/dist/useSsr.d.ts +19 -0
- package/dist/useSsr.js +104 -0
- package/dist/useSsr.js.map +1 -0
- package/dist/vite.d.ts +86 -9
- package/dist/vite.js +827 -24
- package/dist/vite.js.map +1 -1
- package/package.json +13 -1
package/dist/host.js
CHANGED
|
@@ -13,21 +13,32 @@
|
|
|
13
13
|
//
|
|
14
14
|
// const rsc = createRscHandler({ engine, manifest, assets })
|
|
15
15
|
// Bun.serve({ fetch: (req) => rsc(req).then((r) => r ?? new Response('', { status: 404 })) })
|
|
16
|
-
import { allowFor, matchApiRoute, matchIntercept, matchRoute, retentionKey, sharedDepth } from
|
|
17
|
-
import { pathKey, patternKey } from
|
|
18
|
-
import { apiKey } from
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
import { allowFor, matchApiRoute, matchIntercept, matchRoute, retentionKey, sharedDepth, } from "./routing.js";
|
|
17
|
+
import { pathKey, patternKey } from "./prerender.js";
|
|
18
|
+
import { apiKey } from "./apiPrerender.js";
|
|
19
|
+
import { hostPath, hostSegment, routableHost } from "./hostRouting.js";
|
|
20
|
+
import { withRevalidation } from "./revalidate.js";
|
|
21
|
+
/**
|
|
22
|
+
* @internal For a host adapter that embeds the engine. An app imports this
|
|
23
|
+
* from `@rsc-kit/core/revalidate`.
|
|
24
|
+
*/
|
|
25
|
+
export { revalidate } from "./revalidate.js";
|
|
26
|
+
import { currentNotFound, withRedirect } from "./redirect.js";
|
|
27
|
+
import { withCache } from "./cache.js";
|
|
28
|
+
import { withRequest, withResponseDraft } from "./request.js";
|
|
29
|
+
/**
|
|
30
|
+
* @internal For a host adapter that embeds the engine. An app imports this
|
|
31
|
+
* from `@rsc-kit/core/redirect` - the one the guides teach, and the one an
|
|
32
|
+
* editor should offer first.
|
|
33
|
+
*/
|
|
34
|
+
export { redirect } from "./redirect.js";
|
|
25
35
|
// Re-exported, not redefined: routing.ts is the one implementation, shared with
|
|
26
36
|
// the prerenderer and the generated bundle, and this stays the adapter's
|
|
27
37
|
// public surface so a host imports from one place.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
import {
|
|
38
|
+
/** @internal For a host adapter. An app never matches its own routes. */
|
|
39
|
+
export { matchIntercept, matchRoute, sharedDepth } from "./routing.js";
|
|
40
|
+
import { FLIGHT_TYPE, HEADER, HTML_TYPE, PER_CLIENT, REVALIDATE, VARY_ON_RSC, } from "./headers.js";
|
|
41
|
+
import { ServerAuthenticationError, ServerAuthorizationError, } from "./js/errors.js";
|
|
31
42
|
/**
|
|
32
43
|
* The answer to a redirect that was decided before anything was written.
|
|
33
44
|
*
|
|
@@ -53,9 +64,9 @@ import { ServerAuthenticationError, ServerAuthorizationError } from './js/errors
|
|
|
53
64
|
*/
|
|
54
65
|
function inScript(value) {
|
|
55
66
|
return JSON.stringify(value)
|
|
56
|
-
.replace(/</g,
|
|
57
|
-
.replace(/\u2028/g,
|
|
58
|
-
.replace(/\u2029/g,
|
|
67
|
+
.replace(/</g, "\\u003c")
|
|
68
|
+
.replace(/\u2028/g, "\\u2028")
|
|
69
|
+
.replace(/\u2029/g, "\\u2029");
|
|
59
70
|
}
|
|
60
71
|
function redirectResponse(to, isPayloadRequest) {
|
|
61
72
|
if (isPayloadRequest) {
|
|
@@ -84,7 +95,7 @@ function appendLateRedirect(stream, taken) {
|
|
|
84
95
|
// An engine that answered with a finished body rather than a stream has no
|
|
85
96
|
// late window at all: the render was over before this was called, so the
|
|
86
97
|
// caller's own check already saw everything there was to see.
|
|
87
|
-
if (typeof stream?.getReader !==
|
|
98
|
+
if (typeof stream?.getReader !== "function")
|
|
88
99
|
return stream;
|
|
89
100
|
const encoder = new TextEncoder();
|
|
90
101
|
const reader = stream.getReader();
|
|
@@ -129,22 +140,22 @@ function refusalStatus(error) {
|
|
|
129
140
|
// raised by another copy of the module: an app's actions are bundled apart
|
|
130
141
|
// from the engine, and instanceof is false across that seam.
|
|
131
142
|
const name = error?.name;
|
|
132
|
-
if (name ===
|
|
143
|
+
if (name === "ServerAuthenticationError")
|
|
133
144
|
return 401;
|
|
134
|
-
if (name ===
|
|
145
|
+
if (name === "ServerAuthorizationError")
|
|
135
146
|
return 403;
|
|
136
147
|
// A status the host chose — a throttle's 429, a policy's 403 — carried on
|
|
137
148
|
// the error by the transport. Bounded to refusals: a host answering 500
|
|
138
149
|
// should not be able to make this look like a client's fault, and one
|
|
139
150
|
// answering 200 should not turn a failed render into a success.
|
|
140
151
|
const carried = error?.refusalStatus;
|
|
141
|
-
if (typeof carried ===
|
|
152
|
+
if (typeof carried === "number" && carried >= 400 && carried <= 499)
|
|
142
153
|
return carried;
|
|
143
154
|
return null;
|
|
144
155
|
}
|
|
145
156
|
function refusalMessage(error) {
|
|
146
157
|
const message = error?.message;
|
|
147
|
-
return typeof message ===
|
|
158
|
+
return typeof message === "string" && message !== "" ? message : "Refused.";
|
|
148
159
|
}
|
|
149
160
|
/**
|
|
150
161
|
* Whether a browser was told it could post this action.
|
|
@@ -168,10 +179,12 @@ function refusalMessage(error) {
|
|
|
168
179
|
* the preflight.
|
|
169
180
|
*/
|
|
170
181
|
export function actionOriginAllowed(request, url) {
|
|
171
|
-
const origin = request.headers.get(
|
|
182
|
+
const origin = request.headers.get("origin");
|
|
172
183
|
if (!origin)
|
|
173
184
|
return true;
|
|
174
|
-
const expected = request.headers.get(
|
|
185
|
+
const expected = request.headers.get("x-forwarded-host") ??
|
|
186
|
+
request.headers.get("host") ??
|
|
187
|
+
url.host;
|
|
175
188
|
try {
|
|
176
189
|
return new URL(origin).host === expected;
|
|
177
190
|
}
|
|
@@ -190,7 +203,7 @@ const DEFAULT_MAX_ACTION_BODY = 8 * 1024 * 1024;
|
|
|
190
203
|
* dropped, and the caller answers 413 rather than holding the rest.
|
|
191
204
|
*/
|
|
192
205
|
async function readBodyUpTo(request, limit) {
|
|
193
|
-
const declared = Number(request.headers.get(
|
|
206
|
+
const declared = Number(request.headers.get("content-length"));
|
|
194
207
|
if (Number.isFinite(declared) && declared > limit)
|
|
195
208
|
return null;
|
|
196
209
|
if (!request.body)
|
|
@@ -222,11 +235,14 @@ async function readBodyUpTo(request, limit) {
|
|
|
222
235
|
* null. A header is a string anyone can send, and `new URL` throws on one
|
|
223
236
|
* that is not a url; that must not become a 500 on the action endpoint.
|
|
224
237
|
*/
|
|
225
|
-
function refererPath(referer, origin) {
|
|
238
|
+
function refererPath(referer, origin, hosts = []) {
|
|
226
239
|
if (!referer)
|
|
227
240
|
return null;
|
|
228
241
|
try {
|
|
229
|
-
|
|
242
|
+
const parsed = new URL(referer, origin);
|
|
243
|
+
// The page the action was invoked from is routed the way a request for
|
|
244
|
+
// it would be: on a tenant host, the referer's host names the segment.
|
|
245
|
+
return hostPath(parsed.host, parsed.pathname, hosts);
|
|
230
246
|
}
|
|
231
247
|
catch {
|
|
232
248
|
return null;
|
|
@@ -244,6 +260,15 @@ function refererPath(referer, origin) {
|
|
|
244
260
|
* throws on every request instead of being undefined.
|
|
245
261
|
*/
|
|
246
262
|
const MAX_QUERY = 8_000;
|
|
263
|
+
/**
|
|
264
|
+
* The host segment a routed url begins with, by url. Kept beside the url
|
|
265
|
+
* rather than in it, so the pathname stays what a page and a stored file
|
|
266
|
+
* are keyed by, and the matcher alone is told the leading part is the host's.
|
|
267
|
+
*/
|
|
268
|
+
const hostOf = new WeakMap();
|
|
269
|
+
function matchPage(routes, url) {
|
|
270
|
+
return matchRoute(routes, url.pathname, hostOf.get(url) ?? null);
|
|
271
|
+
}
|
|
247
272
|
export function createRscHandler(options) {
|
|
248
273
|
const { engine, assets, version } = options;
|
|
249
274
|
const maxActionBody = options.maxActionBody ?? DEFAULT_MAX_ACTION_BODY;
|
|
@@ -251,7 +276,7 @@ export function createRscHandler(options) {
|
|
|
251
276
|
// closures that use it, and every one of them runs after the throw.
|
|
252
277
|
const manifest = options.manifest ?? engine.manifest?.();
|
|
253
278
|
if (!manifest) {
|
|
254
|
-
throw new Error(
|
|
279
|
+
throw new Error("No route table. Pass `manifest`, or build with a plugin version that embeds one in the bundle.");
|
|
255
280
|
}
|
|
256
281
|
const routes = manifest;
|
|
257
282
|
// Only when this host has functions of its own. Installing unconditionally
|
|
@@ -272,7 +297,7 @@ export function createRscHandler(options) {
|
|
|
272
297
|
return await remote(name, ...args);
|
|
273
298
|
// Louder than returning null: a typo in a server component otherwise
|
|
274
299
|
// renders as missing data with nothing anywhere saying why.
|
|
275
|
-
throw new Error(`No host function named ${JSON.stringify(name)}. Registered: ${Object.keys(fns).join(
|
|
300
|
+
throw new Error(`No host function named ${JSON.stringify(name)}. Registered: ${Object.keys(fns).join(", ") || "(none)"}`);
|
|
276
301
|
});
|
|
277
302
|
}
|
|
278
303
|
async function propsFor(match, request) {
|
|
@@ -283,7 +308,10 @@ export function createRscHandler(options) {
|
|
|
283
308
|
return {
|
|
284
309
|
component: match.route.component,
|
|
285
310
|
props,
|
|
286
|
-
layouts: match.route.layouts.map((component) => ({
|
|
311
|
+
layouts: match.route.layouts.map((component) => ({
|
|
312
|
+
component,
|
|
313
|
+
props: {},
|
|
314
|
+
})),
|
|
287
315
|
loadings: match.route.loadings,
|
|
288
316
|
parallelSlots: match.route.slots,
|
|
289
317
|
// What may be named in X-RSC-Revalidate — see renderRevalidated.
|
|
@@ -298,7 +326,13 @@ export function createRscHandler(options) {
|
|
|
298
326
|
// server is a reasonable thing to do — previewing an export, or one build
|
|
299
327
|
// used both ways — and without this every navigation 404s in the console
|
|
300
328
|
// while the page itself looks fine.
|
|
301
|
-
const payloadName = manifest.build?.payloadName ||
|
|
329
|
+
const payloadName = manifest.build?.payloadName || "";
|
|
330
|
+
/** The site's own hosts; any other host is routed with its segment in front of the path. */
|
|
331
|
+
const siteHosts = manifest.build?.hosts ?? [];
|
|
332
|
+
/** Whether responses say what built them. How they were served is always said. */
|
|
333
|
+
const identify = Boolean(manifest.build?.identify);
|
|
334
|
+
/** How a response was answered, for X-RSC-Kit: a file the build wrote, or a shell of one. */
|
|
335
|
+
const servedFrom = new WeakMap();
|
|
302
336
|
/**
|
|
303
337
|
* The page a payload url belongs to, if this is one.
|
|
304
338
|
*
|
|
@@ -306,14 +340,18 @@ export function createRscHandler(options) {
|
|
|
306
340
|
* built for export asks for index.seg1.rsc when it already holds a layout,
|
|
307
341
|
* and matching only the plain name leaves that as a 404 no page reports.
|
|
308
342
|
*/
|
|
309
|
-
const payloadNames = new RegExp(
|
|
343
|
+
const payloadNames = new RegExp("/" +
|
|
344
|
+
payloadName
|
|
345
|
+
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
346
|
+
.replace(/^index\\\./, "index(\\.seg\\d+)?\\.") +
|
|
347
|
+
"$");
|
|
310
348
|
function pageForPayload(pathname) {
|
|
311
|
-
if (payloadName ===
|
|
349
|
+
if (payloadName === "")
|
|
312
350
|
return null;
|
|
313
351
|
const match = payloadNames.exec(pathname);
|
|
314
352
|
if (!match)
|
|
315
353
|
return null;
|
|
316
|
-
return pathname.slice(0, match.index) ||
|
|
354
|
+
return pathname.slice(0, match.index) || "/";
|
|
317
355
|
}
|
|
318
356
|
// One memo table per request, opened at the outermost point so that
|
|
319
357
|
// everything below shares it: middleware, layouts, the page, and an action. A
|
|
@@ -334,7 +372,7 @@ export function createRscHandler(options) {
|
|
|
334
372
|
// Set-Cookie is applied below, once per cookie: iterating a Headers
|
|
335
373
|
// gives it joined in some runtimes and per-cookie in others, and a
|
|
336
374
|
// joined one is a single malformed cookie the browser discards.
|
|
337
|
-
if (name.toLowerCase() ===
|
|
375
|
+
if (name.toLowerCase() === "set-cookie")
|
|
338
376
|
return;
|
|
339
377
|
// set, not append: a middleware asking for a header means that
|
|
340
378
|
// value, not that value added to whatever the host already chose.
|
|
@@ -342,20 +380,52 @@ export function createRscHandler(options) {
|
|
|
342
380
|
});
|
|
343
381
|
// Appended, because several cookies are several headers.
|
|
344
382
|
for (const cookie of collected.getSetCookie()) {
|
|
345
|
-
response.headers.append(
|
|
383
|
+
response.headers.append("Set-Cookie", cookie);
|
|
346
384
|
}
|
|
385
|
+
// How this answer was served - the header a developer reads in the
|
|
386
|
+
// Network tab, the way X-Nextjs-Cache is. Always: it names no
|
|
387
|
+
// product, and a CDN rule or a check can key on it.
|
|
388
|
+
response.headers.set("X-RSC-Kit", servedFrom.get(response) ?? "rendered");
|
|
389
|
+
// What built it. The name only, never the version - a version in
|
|
390
|
+
// every response is what a vulnerability scanner filters on - and
|
|
391
|
+
// off for a team whose policy strips every framework identifier.
|
|
392
|
+
if (identify)
|
|
393
|
+
response.headers.set("X-Powered-By", "rsc-kit");
|
|
347
394
|
return response;
|
|
348
395
|
})));
|
|
349
396
|
};
|
|
350
397
|
async function route(request) {
|
|
351
398
|
let url = new URL(request.url);
|
|
399
|
+
// A host other than the site's own is matched with its segment in front
|
|
400
|
+
// of the path, once, here: everything below - the api match, the page
|
|
401
|
+
// match, the stored answer, the payload - sees that path and nothing
|
|
402
|
+
// else knows a host was involved. The visitor's url is untouched.
|
|
403
|
+
// Compared against X-Forwarded-Host first, for the same reason the
|
|
404
|
+
// action's origin check is: a proxied deployment is the common one.
|
|
405
|
+
// Only when a route can begin with that segment - a [domain] directory or
|
|
406
|
+
// one named for the host - so an app with a metadataBase and no tenant
|
|
407
|
+
// tree routes every host by path, and a proxy forwarding by an internal
|
|
408
|
+
// name is not read as a tenant.
|
|
409
|
+
if (siteHosts.length > 0) {
|
|
410
|
+
const host = request.headers.get("x-forwarded-host") ??
|
|
411
|
+
request.headers.get("host") ??
|
|
412
|
+
url.host;
|
|
413
|
+
const segment = hostSegment(host, siteHosts);
|
|
414
|
+
if (segment !== null && routableHost(segment, routes.routes)) {
|
|
415
|
+
url = new URL(hostPath(host, url.pathname, siteHosts) + url.search, url.origin);
|
|
416
|
+
hostOf.set(url, segment);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
352
419
|
const asPayload = pageForPayload(url.pathname);
|
|
353
420
|
if (asPayload !== null) {
|
|
354
421
|
// Rewritten to the page it is asking about, with the header the rest of
|
|
355
422
|
// this handler reads — one path through, however the client asked.
|
|
423
|
+
const host = hostOf.get(url);
|
|
356
424
|
url = new URL(asPayload + url.search, url.origin);
|
|
425
|
+
if (host !== undefined)
|
|
426
|
+
hostOf.set(url, host);
|
|
357
427
|
const headers = new Headers(request.headers);
|
|
358
|
-
headers.set(HEADER.rsc,
|
|
428
|
+
headers.set(HEADER.rsc, "1");
|
|
359
429
|
request = new Request(url, { method: request.method, headers });
|
|
360
430
|
}
|
|
361
431
|
if (assets) {
|
|
@@ -363,9 +433,9 @@ export function createRscHandler(options) {
|
|
|
363
433
|
if (asset)
|
|
364
434
|
return asset;
|
|
365
435
|
}
|
|
366
|
-
if (request.method ===
|
|
436
|
+
if (request.method === "POST" && url.pathname === HEADER.actionPath) {
|
|
367
437
|
if (!actionOriginAllowed(request, url)) {
|
|
368
|
-
return new Response(
|
|
438
|
+
return new Response("Cross-origin action", { status: 403 });
|
|
369
439
|
}
|
|
370
440
|
return await handleAction(request, url);
|
|
371
441
|
}
|
|
@@ -385,13 +455,13 @@ export function createRscHandler(options) {
|
|
|
385
455
|
return stored;
|
|
386
456
|
return await engine.handleApiRoute(api.route.name, request, api.params, allowFor(api.route));
|
|
387
457
|
}
|
|
388
|
-
if (request.method ===
|
|
458
|
+
if (request.method === "GET" && url.pathname === HEADER.queryPath) {
|
|
389
459
|
// Same check as an action, for a smaller reason: a cross-origin page
|
|
390
460
|
// cannot read this answer — CORS sees to that — but it can still cause
|
|
391
461
|
// the read to run with the visitor's cookies. A query is side-effect
|
|
392
462
|
// free by contract, so this is depth rather than the only defence.
|
|
393
463
|
if (!actionOriginAllowed(request, url)) {
|
|
394
|
-
return new Response(
|
|
464
|
+
return new Response("Cross-origin query", { status: 403 });
|
|
395
465
|
}
|
|
396
466
|
return await handleQuery(request, url);
|
|
397
467
|
}
|
|
@@ -400,12 +470,12 @@ export function createRscHandler(options) {
|
|
|
400
470
|
if (options.prerendered && url.pathname === HEADER.pprShellPath) {
|
|
401
471
|
return await servePprShell(request, url, options.prerendered);
|
|
402
472
|
}
|
|
403
|
-
if (request.method ===
|
|
473
|
+
if (request.method === "POST" &&
|
|
404
474
|
options.prerendered &&
|
|
405
475
|
url.pathname === HEADER.pprResumePath) {
|
|
406
476
|
return await servePprResume(request, url, options.prerendered);
|
|
407
477
|
}
|
|
408
|
-
if (request.method !==
|
|
478
|
+
if (request.method !== "GET" && request.method !== "HEAD")
|
|
409
479
|
return null;
|
|
410
480
|
// One named region of this page, asked for without mutating anything to
|
|
411
481
|
// earn it. What an action invalidated does not come through here — that
|
|
@@ -427,7 +497,7 @@ export function createRscHandler(options) {
|
|
|
427
497
|
// something smaller than one: answering a named region with the whole
|
|
428
498
|
// document puts the entire page inside that region, and answering an
|
|
429
499
|
// interception with it replaces the page the modal was opening over.
|
|
430
|
-
const match =
|
|
500
|
+
const match = matchPage(routes, url);
|
|
431
501
|
if (options.prerendered) {
|
|
432
502
|
// A guarded route can still be frozen: whether the content is the same
|
|
433
503
|
// for everyone, and whether this caller may see it, are different
|
|
@@ -437,13 +507,20 @@ export function createRscHandler(options) {
|
|
|
437
507
|
if (refusal)
|
|
438
508
|
return refusal;
|
|
439
509
|
const frozen = await servePrerendered(request, url, options.prerendered);
|
|
510
|
+
// A whole page from a file, or a shell of one with the holes rendered
|
|
511
|
+
// now: the header says which. servePrerendered marks the shell.
|
|
512
|
+
if (frozen && !servedFrom.has(frozen))
|
|
513
|
+
servedFrom.set(frozen, "stored");
|
|
440
514
|
if (frozen)
|
|
441
515
|
return frozen;
|
|
442
516
|
}
|
|
443
517
|
if (!match)
|
|
444
518
|
return null;
|
|
445
519
|
const props = await propsFor(match, request);
|
|
446
|
-
const layouts = match.route.layouts.map((component) => ({
|
|
520
|
+
const layouts = match.route.layouts.map((component) => ({
|
|
521
|
+
component,
|
|
522
|
+
props: {},
|
|
523
|
+
}));
|
|
447
524
|
const chain = match.route.layouts;
|
|
448
525
|
// A payload request says so with a header on the page's own url, so one
|
|
449
526
|
// route serves both the document and the navigation that follows it.
|
|
@@ -457,7 +534,6 @@ export function createRscHandler(options) {
|
|
|
457
534
|
// still available. Nothing is buffered to make that true.
|
|
458
535
|
let htmlStream;
|
|
459
536
|
try {
|
|
460
|
-
;
|
|
461
537
|
({ htmlStream } = await engine.handleRscHtmlStream(match.route.component, props, layouts, match.route.loadings, match.route.slots, {}, undefined, url.pathname,
|
|
462
538
|
// A route that ships no runtime gets no bootstrap and no segment
|
|
463
539
|
// boundary — the boundary is itself a client component, so leaving
|
|
@@ -506,10 +582,12 @@ export function createRscHandler(options) {
|
|
|
506
582
|
return null;
|
|
507
583
|
return new Response(appendLateRedirect(htmlStream, taken), {
|
|
508
584
|
headers: withVersion({
|
|
509
|
-
|
|
510
|
-
[HEADER.layouts]: chain.join(
|
|
585
|
+
"Content-Type": HTML_TYPE,
|
|
586
|
+
[HEADER.layouts]: chain.join(","),
|
|
511
587
|
Vary: VARY_ON_RSC,
|
|
512
|
-
|
|
588
|
+
"Cache-Control": match.route.middleware?.length
|
|
589
|
+
? PER_CLIENT
|
|
590
|
+
: REVALIDATE,
|
|
513
591
|
}),
|
|
514
592
|
});
|
|
515
593
|
});
|
|
@@ -522,7 +600,6 @@ export function createRscHandler(options) {
|
|
|
522
600
|
let stream;
|
|
523
601
|
let segmentDepth;
|
|
524
602
|
try {
|
|
525
|
-
;
|
|
526
603
|
({ stream, segmentDepth } = await engine.handleRscStream(match.route.component, props, layouts, match.route.loadings, match.route.slots, {}, from, url.pathname));
|
|
527
604
|
}
|
|
528
605
|
catch (error) {
|
|
@@ -550,11 +627,11 @@ export function createRscHandler(options) {
|
|
|
550
627
|
return redirectResponse(early, true);
|
|
551
628
|
return new Response(stream, {
|
|
552
629
|
headers: withVersion({
|
|
553
|
-
|
|
630
|
+
"Content-Type": FLIGHT_TYPE,
|
|
554
631
|
[HEADER.segmentDepth]: String(segmentDepth),
|
|
555
|
-
[HEADER.layouts]: chain.join(
|
|
632
|
+
[HEADER.layouts]: chain.join(","),
|
|
556
633
|
Vary: VARY_ON_RSC,
|
|
557
|
-
|
|
634
|
+
"Cache-Control": PER_CLIENT,
|
|
558
635
|
}),
|
|
559
636
|
});
|
|
560
637
|
});
|
|
@@ -602,7 +679,7 @@ export function createRscHandler(options) {
|
|
|
602
679
|
async function frozenApi(request, url, api) {
|
|
603
680
|
if (!options.prerendered)
|
|
604
681
|
return null;
|
|
605
|
-
if (request.method !==
|
|
682
|
+
if (request.method !== "GET" && request.method !== "HEAD")
|
|
606
683
|
return null;
|
|
607
684
|
if (api.route.middleware.length > 0)
|
|
608
685
|
return null;
|
|
@@ -628,32 +705,35 @@ export function createRscHandler(options) {
|
|
|
628
705
|
// guessing on the unsafe side.
|
|
629
706
|
if (url.search && frozen.varies !== false)
|
|
630
707
|
return null;
|
|
631
|
-
|
|
708
|
+
const answer = new Response(request.method === "HEAD" ? null : frozen.body, {
|
|
632
709
|
status: frozen.status,
|
|
633
710
|
headers: withVersion(Object.fromEntries(frozen.headers)),
|
|
634
711
|
});
|
|
712
|
+
servedFrom.set(answer, "stored");
|
|
713
|
+
return answer;
|
|
635
714
|
}
|
|
636
715
|
async function servePprShell(request, url, read) {
|
|
637
|
-
if (request.method !==
|
|
716
|
+
if (request.method !== "GET" && request.method !== "HEAD")
|
|
638
717
|
return null;
|
|
639
|
-
const target = url.searchParams.get(
|
|
640
|
-
if (!target || !target.startsWith(
|
|
641
|
-
return new Response(
|
|
718
|
+
const target = url.searchParams.get("url");
|
|
719
|
+
if (!target || !target.startsWith("/")) {
|
|
720
|
+
return new Response("A url is required", { status: 400 });
|
|
642
721
|
}
|
|
643
722
|
// Matched from the url asked for, never from anything else the caller sent.
|
|
644
723
|
const route = matchRoute(routes, new URL(target, url.origin).pathname);
|
|
645
724
|
if (!route)
|
|
646
|
-
return new Response(
|
|
725
|
+
return new Response("No such page", { status: 404 });
|
|
647
726
|
if (route.route.middleware?.length) {
|
|
648
|
-
return new Response(
|
|
727
|
+
return new Response("This route is not edge-cacheable", { status: 404 });
|
|
649
728
|
}
|
|
650
729
|
const key = pathKey(new URL(target, url.origin).pathname);
|
|
651
|
-
const shell = (await read(`${key}.ppr.html`)) ??
|
|
730
|
+
const shell = (await read(`${key}.ppr.html`)) ??
|
|
731
|
+
(await read(`${patternKey(route.route)}.ppr.html`));
|
|
652
732
|
if (shell === null)
|
|
653
|
-
return new Response(
|
|
733
|
+
return new Response("No shell for this page", { status: 404 });
|
|
654
734
|
return new Response(JSON.stringify({ shell, version: version ?? null }), {
|
|
655
735
|
headers: withVersion({
|
|
656
|
-
|
|
736
|
+
"Content-Type": "application/json",
|
|
657
737
|
// Deliberately NOT the REVALIDATE the rest of the host sends. That is
|
|
658
738
|
// `max-age=0, must-revalidate`, which a cache honours by treating the
|
|
659
739
|
// entry as stale the moment it arrives — an edge would store this and
|
|
@@ -665,7 +745,7 @@ export function createRscHandler(options) {
|
|
|
665
745
|
// route never reaches here at all, so it is genuinely cacheable. An
|
|
666
746
|
// hour bounds how long a deploy can be served against a stale shell;
|
|
667
747
|
// the version below catches it sooner than that.
|
|
668
|
-
|
|
748
|
+
"Cache-Control": "public, max-age=3600",
|
|
669
749
|
}),
|
|
670
750
|
});
|
|
671
751
|
}
|
|
@@ -683,21 +763,21 @@ export function createRscHandler(options) {
|
|
|
683
763
|
* gets an anonymous answer.
|
|
684
764
|
*/
|
|
685
765
|
async function servePprResume(request, url, read) {
|
|
686
|
-
const target = url.searchParams.get(
|
|
687
|
-
if (!target || !target.startsWith(
|
|
688
|
-
return new Response(
|
|
766
|
+
const target = url.searchParams.get("url");
|
|
767
|
+
if (!target || !target.startsWith("/")) {
|
|
768
|
+
return new Response("A url is required", { status: 400 });
|
|
689
769
|
}
|
|
690
770
|
const pathname = new URL(target, url.origin).pathname;
|
|
691
771
|
const route = matchRoute(routes, pathname);
|
|
692
772
|
if (!route)
|
|
693
|
-
return new Response(
|
|
773
|
+
return new Response("No such page", { status: 404 });
|
|
694
774
|
// The same refusal the document would get, before a byte of the holes is
|
|
695
775
|
// rendered. Nothing below runs for a caller this turns away.
|
|
696
776
|
const refusal = await refuseUnlessAllowed(request, route);
|
|
697
777
|
if (refusal)
|
|
698
778
|
return refusal;
|
|
699
779
|
if (!engine.handleRscResume) {
|
|
700
|
-
return new Response(
|
|
780
|
+
return new Response("This engine cannot resume", { status: 500 });
|
|
701
781
|
}
|
|
702
782
|
const key = pathKey(pathname);
|
|
703
783
|
const pattern = patternKey(route.route);
|
|
@@ -710,8 +790,8 @@ export function createRscHandler(options) {
|
|
|
710
790
|
// difference between this and the protocol it is modelled on.
|
|
711
791
|
const state = shellKey === null ? null : await read(`${shellKey}.postponed.json`);
|
|
712
792
|
if (state === null)
|
|
713
|
-
return new Response(
|
|
714
|
-
const { htmlStream } = await engine.handleRscResume(route.route.component, route.params, route.route.layouts.map((component) => ({ component, props: {} })), route.route.loadings, route.route.slots, {}, JSON.parse(state), undefined, shellKey === key ? pathname :
|
|
793
|
+
return new Response("Nothing to resume", { status: 404 });
|
|
794
|
+
const { htmlStream } = await engine.handleRscResume(route.route.component, route.params, route.route.layouts.map((component) => ({ component, props: {} })), route.route.loadings, route.route.slots, {}, JSON.parse(state), undefined, shellKey === key ? pathname : "");
|
|
715
795
|
// Carries the build version so a caller holding a cached shell can tell
|
|
716
796
|
// that it no longer belongs to this origin. A shell from an older build
|
|
717
797
|
// does not make the resume fail — it replays against slots that have moved
|
|
@@ -719,9 +799,9 @@ export function createRscHandler(options) {
|
|
|
719
799
|
// else would ever report it.
|
|
720
800
|
return new Response(htmlStream, {
|
|
721
801
|
headers: withVersion({
|
|
722
|
-
|
|
802
|
+
"Content-Type": HTML_TYPE,
|
|
723
803
|
// Rendered for whoever asked. Never cacheable.
|
|
724
|
-
|
|
804
|
+
"Cache-Control": PER_CLIENT,
|
|
725
805
|
}),
|
|
726
806
|
});
|
|
727
807
|
}
|
|
@@ -740,15 +820,17 @@ export function createRscHandler(options) {
|
|
|
740
820
|
// pattern. Nothing in a shell varies by param, so one shell serves every
|
|
741
821
|
// url its route matches — which is the only way a route whose urls were
|
|
742
822
|
// never listed gets anything frozen at all.
|
|
743
|
-
const route =
|
|
823
|
+
const route = matchPage(routes, url);
|
|
744
824
|
const whole = await read(`${key}.html`);
|
|
745
825
|
// A whole page is finished. Nothing to resume, nothing to render.
|
|
746
826
|
if (whole !== null) {
|
|
747
827
|
return new Response(whole, {
|
|
748
828
|
headers: withVersion({
|
|
749
|
-
|
|
829
|
+
"Content-Type": HTML_TYPE,
|
|
750
830
|
Vary: VARY_ON_RSC,
|
|
751
|
-
|
|
831
|
+
"Cache-Control": route?.route.middleware?.length
|
|
832
|
+
? PER_CLIENT
|
|
833
|
+
: REVALIDATE,
|
|
752
834
|
}),
|
|
753
835
|
});
|
|
754
836
|
}
|
|
@@ -785,7 +867,7 @@ export function createRscHandler(options) {
|
|
|
785
867
|
// A shell found under the route's pattern was frozen for no particular
|
|
786
868
|
// url, so it was rendered with no page key. Handing one over now would
|
|
787
869
|
// key the tree differently from the one being resumed.
|
|
788
|
-
shellKey === key ? url.pathname :
|
|
870
|
+
shellKey === key ? url.pathname : "");
|
|
789
871
|
// The shell first, then whatever the resume writes. React's own script
|
|
790
872
|
// travels with the resumed segments and moves them into place, so this is
|
|
791
873
|
// a plain concatenation and the holes land without hydration.
|
|
@@ -806,15 +888,17 @@ export function createRscHandler(options) {
|
|
|
806
888
|
}
|
|
807
889
|
},
|
|
808
890
|
});
|
|
809
|
-
|
|
891
|
+
const withHoles = new Response(body, {
|
|
810
892
|
headers: withVersion({
|
|
811
|
-
|
|
893
|
+
"Content-Type": HTML_TYPE,
|
|
812
894
|
Vary: VARY_ON_RSC,
|
|
813
895
|
// The shell is cacheable; this response is not. It carries the holes,
|
|
814
896
|
// which were rendered for whoever asked.
|
|
815
|
-
|
|
897
|
+
"Cache-Control": PER_CLIENT,
|
|
816
898
|
}),
|
|
817
899
|
});
|
|
900
|
+
servedFrom.set(withHoles, "shell");
|
|
901
|
+
return withHoles;
|
|
818
902
|
}
|
|
819
903
|
// Only the document is ever served frozen for a shell. The payload is what
|
|
820
904
|
// fills it in, and it has to be rendered now — answering with a frozen one
|
|
@@ -836,11 +920,11 @@ export function createRscHandler(options) {
|
|
|
836
920
|
return null;
|
|
837
921
|
return new Response(payload, {
|
|
838
922
|
headers: withVersion({
|
|
839
|
-
|
|
923
|
+
"Content-Type": FLIGHT_TYPE,
|
|
840
924
|
[HEADER.segmentDepth]: String(variant ? shared : 0),
|
|
841
|
-
[HEADER.layouts]: chain.join(
|
|
925
|
+
[HEADER.layouts]: chain.join(","),
|
|
842
926
|
Vary: VARY_ON_RSC,
|
|
843
|
-
|
|
927
|
+
"Cache-Control": PER_CLIENT,
|
|
844
928
|
}),
|
|
845
929
|
});
|
|
846
930
|
}
|
|
@@ -861,7 +945,8 @@ export function createRscHandler(options) {
|
|
|
861
945
|
// until the build froze the page, then silently stop.
|
|
862
946
|
if (!match)
|
|
863
947
|
return null;
|
|
864
|
-
const guarded = (match.route.middleware?.length ?? 0) > 0 ||
|
|
948
|
+
const guarded = (match.route.middleware?.length ?? 0) > 0 ||
|
|
949
|
+
(match.route.hostMiddleware?.length ?? 0) > 0;
|
|
865
950
|
if (!guarded)
|
|
866
951
|
return null;
|
|
867
952
|
// A route that declares middleware and an engine that cannot run it is not
|
|
@@ -870,8 +955,8 @@ export function createRscHandler(options) {
|
|
|
870
955
|
// guarded page. Reachable when a host runs a bundle built by an older
|
|
871
956
|
// plugin, where the export did not exist.
|
|
872
957
|
if (!engine.runRouteMiddleware) {
|
|
873
|
-
return new Response(
|
|
874
|
-
|
|
958
|
+
return new Response("This route declares middleware, and the engine cannot run it. " +
|
|
959
|
+
"Rebuild the app against the current @rsc-kit/core.", { status: 500 });
|
|
875
960
|
}
|
|
876
961
|
const asPayload = request.headers.get(HEADER.rsc) !== null;
|
|
877
962
|
return await withRedirect(async (taken) => {
|
|
@@ -896,17 +981,16 @@ export function createRscHandler(options) {
|
|
|
896
981
|
}
|
|
897
982
|
async function handleRevalidate(request, url, target) {
|
|
898
983
|
if (!engine.handleRscRevalidate) {
|
|
899
|
-
return new Response(
|
|
984
|
+
return new Response("This build cannot revalidate", { status: 501 });
|
|
900
985
|
}
|
|
901
|
-
const match =
|
|
986
|
+
const match = matchPage(routes, url);
|
|
902
987
|
if (!match)
|
|
903
|
-
return new Response(
|
|
988
|
+
return new Response("No such page", { status: 404 });
|
|
904
989
|
// Scoped like the render paths: a guard above the target may refuse, and
|
|
905
990
|
// that refusal is an answer rather than a failure.
|
|
906
991
|
return await withRedirect(async (taken) => {
|
|
907
992
|
let rscPayload;
|
|
908
993
|
try {
|
|
909
|
-
;
|
|
910
994
|
({ rscPayload } = await engine.handleRscRevalidate(target, pageContext(match, await propsFor(match, request))));
|
|
911
995
|
}
|
|
912
996
|
catch (error) {
|
|
@@ -920,11 +1004,11 @@ export function createRscHandler(options) {
|
|
|
920
1004
|
return redirectResponse(refused, true);
|
|
921
1005
|
return new Response(rscPayload, {
|
|
922
1006
|
headers: withVersion({
|
|
923
|
-
|
|
1007
|
+
"Content-Type": FLIGHT_TYPE,
|
|
924
1008
|
// Echoed so the client can tell which region it is holding.
|
|
925
1009
|
[HEADER.revalidate]: target,
|
|
926
1010
|
Vary: VARY_ON_RSC,
|
|
927
|
-
|
|
1011
|
+
"Cache-Control": "private, no-store",
|
|
928
1012
|
}),
|
|
929
1013
|
});
|
|
930
1014
|
});
|
|
@@ -932,7 +1016,7 @@ export function createRscHandler(options) {
|
|
|
932
1016
|
async function handleIntercept(request, url, slot) {
|
|
933
1017
|
const intercept = matchIntercept(routes, url.pathname, slot);
|
|
934
1018
|
if (!intercept)
|
|
935
|
-
return new Response(
|
|
1019
|
+
return new Response("No interceptor for this url", { status: 404 });
|
|
936
1020
|
// The guards of the route being intercepted, before anything is rendered.
|
|
937
1021
|
//
|
|
938
1022
|
// An interceptor exists to show the same resource as the route it stands in
|
|
@@ -944,18 +1028,18 @@ export function createRscHandler(options) {
|
|
|
944
1028
|
//
|
|
945
1029
|
// Derived from the url, never from X-RSC-Referer. The referer is a header
|
|
946
1030
|
// the caller writes, and guarding by it means the caller picks the guard.
|
|
947
|
-
const intercepted =
|
|
1031
|
+
const intercepted = matchPage(routes, url);
|
|
948
1032
|
// Nothing to guard means nothing to serve. A url that matches an
|
|
949
1033
|
// interceptor but no route has no middleware chain to consult, so there is
|
|
950
1034
|
// no way to know whether this caller may see it — and an interceptor
|
|
951
1035
|
// stands in for a route, so a url with no route behind it is not a page
|
|
952
1036
|
// anyone was entitled to open a modal over.
|
|
953
1037
|
if (!intercepted)
|
|
954
|
-
return new Response(
|
|
1038
|
+
return new Response("No such page", { status: 404 });
|
|
955
1039
|
const refusal = await refuseUnlessAllowed(request, intercepted);
|
|
956
1040
|
if (refusal)
|
|
957
1041
|
return refusal;
|
|
958
|
-
const from = refererPath(request.headers.get(HEADER.referer), url.origin);
|
|
1042
|
+
const from = refererPath(request.headers.get(HEADER.referer), url.origin, siteHosts);
|
|
959
1043
|
const under = from ? matchRoute(routes, from) : null;
|
|
960
1044
|
// Without a page to open over there is nothing to intercept: render the
|
|
961
1045
|
// interceptor on its own rather than answering with the wrong page.
|
|
@@ -986,25 +1070,25 @@ export function createRscHandler(options) {
|
|
|
986
1070
|
});
|
|
987
1071
|
return new Response(rscPayload, {
|
|
988
1072
|
headers: withVersion({
|
|
989
|
-
|
|
1073
|
+
"Content-Type": FLIGHT_TYPE,
|
|
990
1074
|
// Says what this payload is, so the client puts it in the slot
|
|
991
1075
|
// instead of treating it as a segment of the page.
|
|
992
1076
|
[HEADER.revalidate]: slot,
|
|
993
1077
|
Vary: VARY_ON_RSC,
|
|
994
1078
|
// Per-client by construction: which region this is was chosen by a
|
|
995
1079
|
// request header, so a shared cache has nothing useful to key on.
|
|
996
|
-
|
|
1080
|
+
"Cache-Control": "private, no-store",
|
|
997
1081
|
}),
|
|
998
1082
|
});
|
|
999
1083
|
}
|
|
1000
1084
|
const { stream, segmentDepth } = await engine.handleRscStream(component, props, chain.map((layout) => ({ component: layout, props: {} })), loadings, slots, {}, sharedDepth(request.headers.get(HEADER.segments), chain), retentionKey(url.pathname, slot));
|
|
1001
1085
|
return new Response(stream, {
|
|
1002
1086
|
headers: withVersion({
|
|
1003
|
-
|
|
1087
|
+
"Content-Type": FLIGHT_TYPE,
|
|
1004
1088
|
[HEADER.segmentDepth]: String(segmentDepth),
|
|
1005
|
-
[HEADER.layouts]: chain.join(
|
|
1089
|
+
[HEADER.layouts]: chain.join(","),
|
|
1006
1090
|
Vary: VARY_ON_RSC,
|
|
1007
|
-
|
|
1091
|
+
"Cache-Control": PER_CLIENT,
|
|
1008
1092
|
}),
|
|
1009
1093
|
});
|
|
1010
1094
|
}
|
|
@@ -1022,8 +1106,8 @@ export function createRscHandler(options) {
|
|
|
1022
1106
|
// A route that declares middleware and an engine that cannot run it is not
|
|
1023
1107
|
// "no middleware" — it is a check that silently does not happen.
|
|
1024
1108
|
if (!engine.runRouteMiddleware) {
|
|
1025
|
-
return new Response(
|
|
1026
|
-
|
|
1109
|
+
return new Response("This route declares middleware, and the engine cannot run it. " +
|
|
1110
|
+
"Rebuild the app against the current @rsc-kit/core.", { status: 500 });
|
|
1027
1111
|
}
|
|
1028
1112
|
return await withRedirect(async (taken) => {
|
|
1029
1113
|
try {
|
|
@@ -1034,9 +1118,9 @@ export function createRscHandler(options) {
|
|
|
1034
1118
|
// followed, so a client can decide for itself.
|
|
1035
1119
|
const redirected = taken();
|
|
1036
1120
|
if (redirected) {
|
|
1037
|
-
return new Response(
|
|
1121
|
+
return new Response("Unauthorized", {
|
|
1038
1122
|
status: 401,
|
|
1039
|
-
headers: {
|
|
1123
|
+
headers: { "X-RSC-Redirect": redirected.location },
|
|
1040
1124
|
});
|
|
1041
1125
|
}
|
|
1042
1126
|
// A visitor who may not use this endpoint has not caused a server
|
|
@@ -1046,13 +1130,15 @@ export function createRscHandler(options) {
|
|
|
1046
1130
|
const status = refusalStatus(error);
|
|
1047
1131
|
if (status === null)
|
|
1048
1132
|
throw error;
|
|
1049
|
-
return new Response(status === 401 ?
|
|
1133
|
+
return new Response(status === 401 ? "Unauthorized" : "Forbidden", {
|
|
1134
|
+
status,
|
|
1135
|
+
});
|
|
1050
1136
|
}
|
|
1051
1137
|
const redirected = taken();
|
|
1052
1138
|
if (redirected) {
|
|
1053
|
-
return new Response(
|
|
1139
|
+
return new Response("Unauthorized", {
|
|
1054
1140
|
status: 401,
|
|
1055
|
-
headers: {
|
|
1141
|
+
headers: { "X-RSC-Redirect": redirected.location },
|
|
1056
1142
|
});
|
|
1057
1143
|
}
|
|
1058
1144
|
return null;
|
|
@@ -1069,64 +1155,73 @@ export function createRscHandler(options) {
|
|
|
1069
1155
|
// and nothing here answers a preflight — the same protection a POST
|
|
1070
1156
|
// carrying X-RSC-Action already had.
|
|
1071
1157
|
if (!request.headers.get(HEADER.query)) {
|
|
1072
|
-
return new Response(
|
|
1158
|
+
return new Response("Missing " + HEADER.query, { status: 400 });
|
|
1073
1159
|
}
|
|
1074
|
-
const id = url.searchParams.get(
|
|
1075
|
-
const args = url.searchParams.get(
|
|
1160
|
+
const id = url.searchParams.get("id");
|
|
1161
|
+
const args = url.searchParams.get("args");
|
|
1076
1162
|
if (!id || args === null)
|
|
1077
|
-
return new Response(
|
|
1163
|
+
return new Response("Missing id or args", { status: 400 });
|
|
1078
1164
|
// Enforced here as well as in the client, because the limit is what keeps
|
|
1079
1165
|
// an attacker from making this endpoint decode megabytes of their payload
|
|
1080
1166
|
// per request.
|
|
1081
1167
|
if (url.search.length > MAX_QUERY)
|
|
1082
|
-
return new Response(
|
|
1168
|
+
return new Response("Query too large", { status: 414 });
|
|
1083
1169
|
const answered = await engine.handleQuery(id, args);
|
|
1084
1170
|
// Unknown id and registered-but-not-a-query are the same answer on purpose.
|
|
1085
1171
|
if (!answered)
|
|
1086
|
-
return new Response(
|
|
1172
|
+
return new Response("No such query", { status: 404 });
|
|
1087
1173
|
// The read refused. Answered as a status with the message in the body, so
|
|
1088
1174
|
// the fetcher rejects with something a person can read — a failure rendered
|
|
1089
1175
|
// into a 200 would reach the browser as React's opaque error instead.
|
|
1090
|
-
if (!(
|
|
1176
|
+
if (!("stream" in answered)) {
|
|
1091
1177
|
return new Response(JSON.stringify({ message: answered.message, errors: answered.errors }), {
|
|
1092
1178
|
status: answered.status,
|
|
1093
|
-
headers: withVersion({
|
|
1179
|
+
headers: withVersion({
|
|
1180
|
+
"Content-Type": "application/json",
|
|
1181
|
+
"Cache-Control": PER_CLIENT,
|
|
1182
|
+
}),
|
|
1094
1183
|
});
|
|
1095
1184
|
}
|
|
1096
1185
|
return new Response(answered.stream, {
|
|
1097
1186
|
headers: withVersion({
|
|
1098
|
-
|
|
1099
|
-
|
|
1187
|
+
"Content-Type": FLIGHT_TYPE,
|
|
1188
|
+
"Cache-Control": answered.cacheControl,
|
|
1100
1189
|
// The answer is narrowed by who is asking whenever a read touches the
|
|
1101
1190
|
// session, and the request that carries that is the cookie. Without
|
|
1102
1191
|
// this a shared cache keyed on the url alone hands one visitor
|
|
1103
1192
|
// another's answer — for any query that opted out of no-store.
|
|
1104
|
-
Vary:
|
|
1193
|
+
Vary: "Cookie, " + HEADER.referer,
|
|
1105
1194
|
}),
|
|
1106
1195
|
});
|
|
1107
1196
|
}
|
|
1108
1197
|
async function handleAction(request, url) {
|
|
1109
1198
|
const actionId = request.headers.get(HEADER.action);
|
|
1110
1199
|
if (!actionId)
|
|
1111
|
-
return new Response(
|
|
1200
|
+
return new Response("Missing X-RSC-Action", { status: 400 });
|
|
1112
1201
|
// The body travels as application/octet-stream so a host that parses
|
|
1113
1202
|
// multipart cannot consume it first; its real type rides in a header.
|
|
1114
1203
|
const body = await readBodyUpTo(request, maxActionBody);
|
|
1115
1204
|
if (body === null) {
|
|
1116
|
-
return new Response(`Action body over ${maxActionBody} bytes`, {
|
|
1205
|
+
return new Response(`Action body over ${maxActionBody} bytes`, {
|
|
1206
|
+
status: 413,
|
|
1207
|
+
});
|
|
1117
1208
|
}
|
|
1118
|
-
const contentType = request.headers.get(HEADER.contentType) ??
|
|
1209
|
+
const contentType = request.headers.get(HEADER.contentType) ?? "text/plain;charset=UTF-8";
|
|
1119
1210
|
// Where it was invoked from, so anything the action invalidates can be
|
|
1120
1211
|
// re-rendered against the page that is actually on screen.
|
|
1121
|
-
const from = refererPath(request.headers.get(HEADER.referer), url.origin);
|
|
1212
|
+
const from = refererPath(request.headers.get(HEADER.referer), url.origin, siteHosts);
|
|
1122
1213
|
const match = from ? matchRoute(routes, from) : null;
|
|
1123
|
-
const page = match
|
|
1214
|
+
const page = match
|
|
1215
|
+
? pageContext(match, await propsFor(match, request))
|
|
1216
|
+
: undefined;
|
|
1124
1217
|
// Scoped to this action: revalidate() called anywhere inside it, at any
|
|
1125
1218
|
// depth, marks here and nowhere else — two requests can be in flight and
|
|
1126
1219
|
// marking is per-request state.
|
|
1127
1220
|
const { stream } = await withRevalidation((taken) => engine.handleAction(actionId, body, contentType, page, taken));
|
|
1128
1221
|
return new Response(stream, {
|
|
1129
|
-
headers: withVersion({
|
|
1222
|
+
headers: withVersion({
|
|
1223
|
+
"Content-Type": "text/x-component; charset=utf-8",
|
|
1224
|
+
}),
|
|
1130
1225
|
});
|
|
1131
1226
|
}
|
|
1132
1227
|
}
|