@timber-js/app 0.2.0-alpha.129 → 0.2.0-alpha.131
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-CdzSSeTh.js → actions-BNhpxrnV.js} +7 -4
- package/dist/_chunks/{actions-CdzSSeTh.js.map → actions-BNhpxrnV.js.map} +1 -1
- package/dist/index.js +15 -2
- package/dist/index.js.map +1 -1
- package/dist/plugins/server-bundle.d.ts.map +1 -1
- package/dist/server/form-data.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/internal.js +1 -1
- package/dist/server/internal.js.map +1 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-render.d.ts +10 -0
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/plugins/server-bundle.ts +27 -2
- package/src/server/form-data.ts +25 -5
- package/src/server/render-timeout.ts +12 -12
- package/src/server/ssr-entry.ts +6 -2
- package/src/server/ssr-render.ts +21 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssr-entry.d.ts","sourceRoot":"","sources":["../../src/server/ssr-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAOH,OAAO,uCAAuC,CAAC;AA4B/C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAsDxD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,SAAS,CAC7B,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"ssr-entry.d.ts","sourceRoot":"","sources":["../../src/server/ssr-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAOH,OAAO,uCAAuC,CAAC;AA4B/C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAsDxD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,SAAS,CAC7B,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,QAAQ,CAAC,CAuMnB;AAED,eAAe,SAAS,CAAC"}
|
|
@@ -39,6 +39,11 @@ export declare function renderSsrStream(element: ReactNode, options?: {
|
|
|
39
39
|
deferSuspenseFor?: number;
|
|
40
40
|
signal?: AbortSignal;
|
|
41
41
|
renderTimeoutMs?: number;
|
|
42
|
+
/** Request info threaded into render-error logs (method/path). */
|
|
43
|
+
requestInfo?: {
|
|
44
|
+
method: string;
|
|
45
|
+
path: string;
|
|
46
|
+
};
|
|
42
47
|
/**
|
|
43
48
|
* Fired synchronously when the render function is about to resolve
|
|
44
49
|
* (i.e. at hold-end / shell-ready). `resolved` reports whether Fizz's
|
|
@@ -62,6 +67,11 @@ export declare function renderSsrNodeStream(element: ReactNode, options?: {
|
|
|
62
67
|
deferSuspenseFor?: number;
|
|
63
68
|
signal?: AbortSignal;
|
|
64
69
|
renderTimeoutMs?: number;
|
|
70
|
+
/** Request info threaded into render-error logs (method/path). */
|
|
71
|
+
requestInfo?: {
|
|
72
|
+
method: string;
|
|
73
|
+
path: string;
|
|
74
|
+
};
|
|
65
75
|
/**
|
|
66
76
|
* Fired synchronously at the moment `renderSsrNodeStream`'s returned
|
|
67
77
|
* Promise is about to resolve. `resolved` reports whether Fizz's
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssr-render.d.ts","sourceRoot":"","sources":["../../src/server/ssr-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAuFvC;;;;;;;;;;;;;GAaG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,SAAS,EAClB,OAAO,CAAC,EAAE;IACR,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;OAMG;IACH,sBAAsB,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;CACtD,GACA,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAErC;AAED,wEAAwE;AACxE,eAAO,MAAM,cAAc,SAAkB,CAAC;AAU9C;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,SAAS,EAClB,OAAO,CAAC,EAAE;IACR,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;CACtD,GACA,OAAO,CAAC,OAAO,aAAa,EAAE,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"ssr-render.d.ts","sourceRoot":"","sources":["../../src/server/ssr-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAuFvC;;;;;;;;;;;;;GAaG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,SAAS,EAClB,OAAO,CAAC,EAAE;IACR,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kEAAkE;IAClE,WAAW,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C;;;;;;OAMG;IACH,sBAAsB,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;CACtD,GACA,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAErC;AAED,wEAAwE;AACxE,eAAO,MAAM,cAAc,SAAkB,CAAC;AAU9C;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,SAAS,EAClB,OAAO,CAAC,EAAE;IACR,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kEAAkE;IAClE,WAAW,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;CACtD,GACA,OAAO,CAAC,OAAO,aAAa,EAAE,QAAQ,CAAC,CAqGzC;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,OAAO,aAAa,EAAE,QAAQ,GACvC,cAAc,CAAC,UAAU,CAAC,CAE5B;AA2GD;;;;;;;;;;;;GAYG;AACH,2CAA2C;AAC3C,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,MAAM,CAAC,EAAE,WAAW,GACnB,cAAc,CAAC,UAAU,CAAC,CA2B5B;AAWD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,cAAc,CAAC,UAAU,CAAC,EACtC,UAAU,EAAE,MAAM,EAClB,eAAe,EAAE,OAAO,GACvB,QAAQ,CASV"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timber-js/app",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.131",
|
|
4
4
|
"description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare-workers",
|
|
@@ -13,6 +13,31 @@
|
|
|
13
13
|
|
|
14
14
|
import type { Plugin } from 'vite';
|
|
15
15
|
|
|
16
|
+
// Build-time-only packages that must never be bundled into the runtime
|
|
17
|
+
// server environments. With `noExternal: true` rolldown otherwise follows
|
|
18
|
+
// the transitive graph through the user's `timber.config.ts` (loaded by the
|
|
19
|
+
// `virtual:timber-cache-handler` dynamic import) into adapter code, dragging
|
|
20
|
+
// build tooling into the prod server bundle. The most visible symptom is
|
|
21
|
+
// rolldown trying to parse `fsevents.node` as JS and failing with
|
|
22
|
+
// `[UNLOADABLE_DEPENDENCY] … stream did not contain valid UTF-8`.
|
|
23
|
+
//
|
|
24
|
+
// - `nitro`, `@mdx-js/rollup` — build-time tooling reachable
|
|
25
|
+
// transitively through adapter modules and timber's own MDX activation.
|
|
26
|
+
// - `rollup` — pulled in by nitro and
|
|
27
|
+
// @mdx-js/rollup; declares `fsevents` as an optional native dep.
|
|
28
|
+
// - `fsevents`, `chokidar` — native/optional deps of
|
|
29
|
+
// rollup/vite. They are macOS-only file watchers with no runtime use
|
|
30
|
+
// in a server bundle.
|
|
31
|
+
//
|
|
32
|
+
// Mirrored on both rsc and ssr environments. See TIM-988.
|
|
33
|
+
const BUILD_ONLY_EXTERNALS: string[] = [
|
|
34
|
+
'nitro',
|
|
35
|
+
'rollup',
|
|
36
|
+
'@mdx-js/rollup',
|
|
37
|
+
'fsevents',
|
|
38
|
+
'chokidar',
|
|
39
|
+
];
|
|
40
|
+
|
|
16
41
|
export function timberServerBundle(): Plugin[] {
|
|
17
42
|
const bundlePlugin: Plugin = {
|
|
18
43
|
name: 'timber-server-bundle',
|
|
@@ -80,11 +105,11 @@ export function timberServerBundle(): Plugin[] {
|
|
|
80
105
|
ssr: { target: 'webworker' },
|
|
81
106
|
environments: {
|
|
82
107
|
rsc: {
|
|
83
|
-
resolve: { noExternal: true },
|
|
108
|
+
resolve: { noExternal: true, external: BUILD_ONLY_EXTERNALS },
|
|
84
109
|
define: serverDefine,
|
|
85
110
|
},
|
|
86
111
|
ssr: {
|
|
87
|
-
resolve: { noExternal: true },
|
|
112
|
+
resolve: { noExternal: true, external: BUILD_ONLY_EXTERNALS },
|
|
88
113
|
define: serverDefine,
|
|
89
114
|
},
|
|
90
115
|
},
|
package/src/server/form-data.ts
CHANGED
|
@@ -29,6 +29,12 @@ export function parseFormData(formData: FormData): Record<string, unknown> {
|
|
|
29
29
|
// Skip React internal fields
|
|
30
30
|
if (key.startsWith('$ACTION_')) continue;
|
|
31
31
|
|
|
32
|
+
// Skip prototype-pollution keys at the top level — assigning to
|
|
33
|
+
// `flat['__proto__']` invokes the prototype setter and can splice
|
|
34
|
+
// attacker-controlled data into `flat`'s prototype chain.
|
|
35
|
+
// The dot-path case is filtered separately in `expandDotPaths`.
|
|
36
|
+
if (DANGEROUS_KEYS.has(key)) continue;
|
|
37
|
+
|
|
32
38
|
const values = formData.getAll(key);
|
|
33
39
|
const processed = values.map(normalizeValue);
|
|
34
40
|
|
|
@@ -82,18 +88,23 @@ function expandDotPaths(flat: Record<string, unknown>): Record<string, unknown>
|
|
|
82
88
|
}
|
|
83
89
|
}
|
|
84
90
|
|
|
85
|
-
// Fast path: no dot-notation keys, return as-is
|
|
91
|
+
// Fast path: no dot-notation keys, return as-is.
|
|
92
|
+
// Top-level dangerous keys are already filtered in `parseFormData` above.
|
|
86
93
|
if (!hasDotPaths) return flat;
|
|
87
94
|
|
|
88
95
|
for (const [key, value] of Object.entries(flat)) {
|
|
89
|
-
|
|
90
|
-
|
|
96
|
+
// Reject any path that contains __proto__ / constructor / prototype.
|
|
97
|
+
// Without this, `__proto__.x=1` writes to Object.prototype because
|
|
98
|
+
// `result['__proto__']` resolves to the prototype object itself.
|
|
99
|
+
const parts = key.split('.');
|
|
100
|
+
if (parts.some((p) => DANGEROUS_KEYS.has(p))) continue;
|
|
101
|
+
|
|
102
|
+
if (parts.length === 1) {
|
|
103
|
+
result[parts[0]] = value;
|
|
91
104
|
continue;
|
|
92
105
|
}
|
|
93
106
|
|
|
94
|
-
const parts = key.split('.');
|
|
95
107
|
let current: Record<string, unknown> = result;
|
|
96
|
-
|
|
97
108
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
98
109
|
const part = parts[i];
|
|
99
110
|
if (current[part] === undefined || current[part] === null) {
|
|
@@ -113,6 +124,15 @@ function expandDotPaths(flat: Record<string, unknown>): Record<string, unknown>
|
|
|
113
124
|
return result;
|
|
114
125
|
}
|
|
115
126
|
|
|
127
|
+
// `__proto__` is the only key in this parser that escapes the dot-path
|
|
128
|
+
// walk's `typeof !== 'object'` reset: accessing `obj['__proto__']` returns
|
|
129
|
+
// `Object.prototype` (itself an object), so the walk steps into the global
|
|
130
|
+
// prototype and the leaf write mutates it. `constructor` resolves to a
|
|
131
|
+
// function (reset fires) and `prototype` is undefined on fresh objects
|
|
132
|
+
// (reset fires), so neither escapes the walk — they parse as legit nested
|
|
133
|
+
// keys and do not mutate anything global.
|
|
134
|
+
const DANGEROUS_KEYS = new Set(['__proto__']);
|
|
135
|
+
|
|
116
136
|
// ─── Coercion Helpers ────────────────────────────────────────────────────
|
|
117
137
|
|
|
118
138
|
/**
|
|
@@ -55,24 +55,20 @@ export function createRenderTimeout(timeoutMs: number, parentSignal?: AbortSigna
|
|
|
55
55
|
const controller = new AbortController();
|
|
56
56
|
const reason = new RenderTimeoutError(timeoutMs, 'RSC stream read timed out');
|
|
57
57
|
|
|
58
|
-
const timer = setTimeout(() =>
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
const timer = setTimeout(() => controller.abort(reason), timeoutMs);
|
|
59
|
+
|
|
60
|
+
let onParentAbort: (() => void) | null = null;
|
|
61
61
|
|
|
62
|
-
// If there's a parent signal (e.g. request abort), chain it
|
|
63
62
|
if (parentSignal) {
|
|
64
63
|
if (parentSignal.aborted) {
|
|
65
64
|
clearTimeout(timer);
|
|
66
65
|
controller.abort(parentSignal.reason);
|
|
67
66
|
} else {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
()
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
},
|
|
74
|
-
{ once: true }
|
|
75
|
-
);
|
|
67
|
+
onParentAbort = () => {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
controller.abort(parentSignal.reason);
|
|
70
|
+
};
|
|
71
|
+
parentSignal.addEventListener('abort', onParentAbort, { once: true });
|
|
76
72
|
}
|
|
77
73
|
}
|
|
78
74
|
|
|
@@ -80,6 +76,10 @@ export function createRenderTimeout(timeoutMs: number, parentSignal?: AbortSigna
|
|
|
80
76
|
signal: controller.signal,
|
|
81
77
|
cancel: () => {
|
|
82
78
|
clearTimeout(timer);
|
|
79
|
+
if (onParentAbort && parentSignal) {
|
|
80
|
+
parentSignal.removeEventListener('abort', onParentAbort);
|
|
81
|
+
onParentAbort = null;
|
|
82
|
+
}
|
|
83
83
|
},
|
|
84
84
|
};
|
|
85
85
|
}
|
package/src/server/ssr-entry.ts
CHANGED
|
@@ -198,6 +198,7 @@ export async function handleSsr(
|
|
|
198
198
|
} = _nodeStreamImports;
|
|
199
199
|
|
|
200
200
|
const renderTimeoutMs = _runtimeConfig.renderTimeoutMs ?? undefined;
|
|
201
|
+
const requestInfo = { method: 'GET', path: navContext.pathname };
|
|
201
202
|
|
|
202
203
|
let nodeHtmlStream: import('node:stream').Readable;
|
|
203
204
|
try {
|
|
@@ -206,12 +207,13 @@ export async function handleSsr(
|
|
|
206
207
|
deferSuspenseFor: navContext.deferSuspenseFor,
|
|
207
208
|
signal: navContext.signal,
|
|
208
209
|
renderTimeoutMs,
|
|
210
|
+
requestInfo,
|
|
209
211
|
onFizzAllReadyResolved: (resolved) => {
|
|
210
212
|
navContext._fizzAllReadyResolved = resolved;
|
|
211
213
|
},
|
|
212
214
|
});
|
|
213
215
|
} catch (renderError) {
|
|
214
|
-
logRenderError({
|
|
216
|
+
logRenderError({ ...requestInfo, error: renderError });
|
|
215
217
|
throw new SsrStreamError(
|
|
216
218
|
'SSR renderToReadableStream failed due to RSC stream error',
|
|
217
219
|
renderError
|
|
@@ -268,6 +270,7 @@ export async function handleSsr(
|
|
|
268
270
|
|
|
269
271
|
// Web Streams path (CF Workers / fallback)
|
|
270
272
|
const renderTimeoutMs = _runtimeConfig.renderTimeoutMs ?? undefined;
|
|
273
|
+
const requestInfo = { method: 'GET', path: navContext.pathname };
|
|
271
274
|
let htmlStream: ReadableStream<Uint8Array>;
|
|
272
275
|
try {
|
|
273
276
|
htmlStream = await renderSsrStream(wrappedElement, {
|
|
@@ -275,12 +278,13 @@ export async function handleSsr(
|
|
|
275
278
|
deferSuspenseFor: navContext.deferSuspenseFor,
|
|
276
279
|
signal: navContext.signal,
|
|
277
280
|
renderTimeoutMs,
|
|
281
|
+
requestInfo,
|
|
278
282
|
onFizzAllReadyResolved: (resolved) => {
|
|
279
283
|
navContext._fizzAllReadyResolved = resolved;
|
|
280
284
|
},
|
|
281
285
|
});
|
|
282
286
|
} catch (renderError) {
|
|
283
|
-
logRenderError({
|
|
287
|
+
logRenderError({ ...requestInfo, error: renderError });
|
|
284
288
|
throw new SsrStreamError(
|
|
285
289
|
'SSR renderToReadableStream failed due to RSC stream error',
|
|
286
290
|
renderError
|
package/src/server/ssr-render.ts
CHANGED
|
@@ -128,6 +128,8 @@ export async function renderSsrStream(
|
|
|
128
128
|
deferSuspenseFor?: number;
|
|
129
129
|
signal?: AbortSignal;
|
|
130
130
|
renderTimeoutMs?: number;
|
|
131
|
+
/** Request info threaded into render-error logs (method/path). */
|
|
132
|
+
requestInfo?: { method: string; path: string };
|
|
131
133
|
/**
|
|
132
134
|
* Fired synchronously when the render function is about to resolve
|
|
133
135
|
* (i.e. at hold-end / shell-ready). `resolved` reports whether Fizz's
|
|
@@ -166,6 +168,8 @@ export async function renderSsrNodeStream(
|
|
|
166
168
|
deferSuspenseFor?: number;
|
|
167
169
|
signal?: AbortSignal;
|
|
168
170
|
renderTimeoutMs?: number;
|
|
171
|
+
/** Request info threaded into render-error logs (method/path). */
|
|
172
|
+
requestInfo?: { method: string; path: string };
|
|
169
173
|
/**
|
|
170
174
|
* Fired synchronously at the moment `renderSsrNodeStream`'s returned
|
|
171
175
|
* Promise is about to resolve. `resolved` reports whether Fizz's
|
|
@@ -178,6 +182,7 @@ export async function renderSsrNodeStream(
|
|
|
178
182
|
const signal = options?.signal;
|
|
179
183
|
const deferMs = options?.deferSuspenseFor;
|
|
180
184
|
const timeoutMs = options?.renderTimeoutMs;
|
|
185
|
+
const requestInfo = options?.requestInfo;
|
|
181
186
|
const onFizzAllReadyResolved = options?.onFizzAllReadyResolved;
|
|
182
187
|
|
|
183
188
|
return new Promise<import('node:stream').Readable>((resolve, reject) => {
|
|
@@ -229,7 +234,11 @@ export async function renderSsrNodeStream(
|
|
|
229
234
|
// Skip React's sanitized production errors — the original was
|
|
230
235
|
// already logged by the RSC onError with full details.
|
|
231
236
|
if (isReactSanitizedError(error)) return;
|
|
232
|
-
logRenderError({
|
|
237
|
+
logRenderError({
|
|
238
|
+
method: requestInfo?.method ?? '',
|
|
239
|
+
path: requestInfo?.path ?? '',
|
|
240
|
+
error,
|
|
241
|
+
});
|
|
233
242
|
},
|
|
234
243
|
});
|
|
235
244
|
|
|
@@ -250,8 +259,8 @@ export async function renderSsrNodeStream(
|
|
|
250
259
|
'abort',
|
|
251
260
|
() => {
|
|
252
261
|
logRenderError({
|
|
253
|
-
method: '',
|
|
254
|
-
path: '',
|
|
262
|
+
method: requestInfo?.method ?? '',
|
|
263
|
+
path: requestInfo?.path ?? '',
|
|
255
264
|
error: new Error(
|
|
256
265
|
`SSR render timed out after ${timeoutMs}ms — aborting. ` +
|
|
257
266
|
'A Suspense component or downstream fetch may be hanging.'
|
|
@@ -293,11 +302,13 @@ async function renderViaReadableStream(
|
|
|
293
302
|
deferSuspenseFor?: number;
|
|
294
303
|
signal?: AbortSignal;
|
|
295
304
|
renderTimeoutMs?: number;
|
|
305
|
+
requestInfo?: { method: string; path: string };
|
|
296
306
|
onFizzAllReadyResolved?: (resolved: boolean) => void;
|
|
297
307
|
}
|
|
298
308
|
): Promise<ReadableStream<Uint8Array>> {
|
|
299
309
|
const signal = options?.signal;
|
|
300
310
|
const timeoutMs = options?.renderTimeoutMs;
|
|
311
|
+
const requestInfo = options?.requestInfo;
|
|
301
312
|
const onFizzAllReadyResolved = options?.onFizzAllReadyResolved;
|
|
302
313
|
|
|
303
314
|
// If a render timeout is configured, create a combined signal that
|
|
@@ -318,8 +329,8 @@ async function renderViaReadableStream(
|
|
|
318
329
|
if (isAbortError(error) || effectiveSignal?.aborted) return;
|
|
319
330
|
if (error instanceof RenderTimeoutError) {
|
|
320
331
|
logRenderError({
|
|
321
|
-
method: '',
|
|
322
|
-
path: '',
|
|
332
|
+
method: requestInfo?.method ?? '',
|
|
333
|
+
path: requestInfo?.path ?? '',
|
|
323
334
|
error: new Error(
|
|
324
335
|
`SSR render timed out after ${timeoutMs}ms — aborting. ` +
|
|
325
336
|
'A Suspense component or downstream fetch may be hanging.'
|
|
@@ -330,7 +341,11 @@ async function renderViaReadableStream(
|
|
|
330
341
|
// Skip React's sanitized production errors — the original was
|
|
331
342
|
// already logged by the RSC onError with full details.
|
|
332
343
|
if (isReactSanitizedError(error)) return;
|
|
333
|
-
logRenderError({
|
|
344
|
+
logRenderError({
|
|
345
|
+
method: requestInfo?.method ?? '',
|
|
346
|
+
path: requestInfo?.path ?? '',
|
|
347
|
+
error,
|
|
348
|
+
});
|
|
334
349
|
},
|
|
335
350
|
});
|
|
336
351
|
} catch (error) {
|