Skip to content

Commit c6f6c80

Browse files
authored
Add custom handleError (#199)
1 parent 0d75e19 commit c6f6c80

1 file changed

Lines changed: 95 additions & 0 deletions

File tree

app/entry.server.tsx

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { PassThrough } from "node:stream";
2+
3+
import type {
4+
AppLoadContext,
5+
EntryContext,
6+
HandleErrorFunction,
7+
} from "react-router";
8+
import { createReadableStreamFromReadable } from "@react-router/node";
9+
import { isRouteErrorResponse, ServerRouter } from "react-router";
10+
import { isbot } from "isbot";
11+
import type { RenderToPipeableStreamOptions } from "react-dom/server";
12+
import { renderToPipeableStream } from "react-dom/server";
13+
14+
export const streamTimeout = 5_000;
15+
16+
export const handleError: HandleErrorFunction = (
17+
error: unknown,
18+
{ request }
19+
) => {
20+
if (request.signal.aborted) {
21+
// Don't log aborted requests - they're expected
22+
return;
23+
}
24+
25+
if (isRouteErrorResponse(error) && error.status === 404) {
26+
// Don't log 404's - they're usually just bot noise
27+
return;
28+
}
29+
30+
if (isRouteErrorResponse(error)) {
31+
console.error(error.status === 500, error.statusText, error.data);
32+
} else {
33+
console.error(error);
34+
}
35+
};
36+
37+
export default function handleRequest(
38+
request: Request,
39+
responseStatusCode: number,
40+
responseHeaders: Headers,
41+
routerContext: EntryContext,
42+
loadContext: AppLoadContext
43+
// If you have middleware enabled:
44+
// loadContext: unstable_RouterContextProvider
45+
) {
46+
return new Promise((resolve, reject) => {
47+
let shellRendered = false;
48+
let userAgent = request.headers.get("user-agent");
49+
50+
// Ensure requests from bots and SPA Mode renders wait for all content to load before responding
51+
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
52+
let readyOption: keyof RenderToPipeableStreamOptions =
53+
(userAgent && isbot(userAgent)) || routerContext.isSpaMode
54+
? "onAllReady"
55+
: "onShellReady";
56+
57+
const { pipe, abort } = renderToPipeableStream(
58+
<ServerRouter context={routerContext} url={request.url} />,
59+
{
60+
[readyOption]() {
61+
shellRendered = true;
62+
const body = new PassThrough();
63+
const stream = createReadableStreamFromReadable(body);
64+
65+
responseHeaders.set("Content-Type", "text/html");
66+
67+
resolve(
68+
new Response(stream, {
69+
headers: responseHeaders,
70+
status: responseStatusCode,
71+
})
72+
);
73+
74+
pipe(body);
75+
},
76+
onShellError(error: unknown) {
77+
reject(error);
78+
},
79+
onError(error: unknown) {
80+
responseStatusCode = 500;
81+
// Log streaming rendering errors from inside the shell. Don't log
82+
// errors encountered during initial shell rendering since they'll
83+
// reject and get logged in handleDocumentRequest.
84+
if (shellRendered) {
85+
console.error(error);
86+
}
87+
},
88+
}
89+
);
90+
91+
// Abort the rendering stream after the `streamTimeout` so it has time to
92+
// flush down the rejected boundaries
93+
setTimeout(abort, streamTimeout + 1000);
94+
});
95+
}

0 commit comments

Comments
 (0)