Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/happy-hornets-judge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"react-router": patch
---

Do not try to use `turbo-stream` to decode CDN errors that never reached the server

- We used to do this but lost this check with the adoption of single fetch
4 changes: 2 additions & 2 deletions integration/error-boundary-v2-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,8 @@ test.describe("ErrorBoundary", () => {
await waitForAndAssert(
page,
app,
"#parent-error",
"Unable to decode turbo-stream response",
"#parent-error-response",
"500 CDN Error!",
);
});
});
Expand Down
6 changes: 3 additions & 3 deletions integration/error-data-request-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ test.describe("ErrorBoundary", () => {
let { status, headers, data } =
await fixture.requestSingleFetchData("/_root.data");
expect(status).toBe(200);
expect(headers.has("X-Remix-Error")).toBe(false);
expect(headers.has("X-Remix-Response")).toBe(true);
expect(data).toEqual({});
});

Expand All @@ -122,7 +122,7 @@ test.describe("ErrorBoundary", () => {
},
);
expect(status).toBe(405);
expect(headers.has("X-Remix-Error")).toBe(false);
expect(headers.has("X-Remix-Response")).toBe(true);
expect(data).toEqual({
error: new ErrorResponseImpl(
405,
Expand Down Expand Up @@ -153,7 +153,7 @@ test.describe("ErrorBoundary", () => {
"/i/match/nothing.data",
);
expect(status).toBe(404);
expect(headers.has("X-Remix-Error")).toBe(false);
expect(headers.has("X-Remix-Response")).toBe(true);
expect(data).toEqual({
root: {
error: new ErrorResponseImpl(
Expand Down
16 changes: 7 additions & 9 deletions packages/react-router/__tests__/server-runtime/server-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,7 @@ describe("shared server runtime", () => {

let result = await handler(request);
expect(result.status).toBe(400);
expect(result.headers.get("X-Remix-Error")).toBe("yes");
expect(headers.has("X-Remix-Response")).toBe(true);
expect((await result.json()).message).toBeTruthy();
});

Expand Down Expand Up @@ -585,7 +585,7 @@ describe("shared server runtime", () => {

let result = await handler(request);
expect(result.status).toBe(403);
expect(result.headers.get("X-Remix-Error")).toBe("yes");
expect(headers.has("X-Remix-Response")).toBe(true);
expect((await result.json()).message).toBeTruthy();
});

Expand All @@ -608,7 +608,7 @@ describe("shared server runtime", () => {

let result = await handler(request);
expect(result.status).toBe(404);
expect(result.headers.get("X-Remix-Error")).toBe("yes");
expect(headers.has("X-Remix-Response")).toBe(true);
expect((await result.json()).message).toBeTruthy();
});

Expand Down Expand Up @@ -670,7 +670,7 @@ describe("shared server runtime", () => {
let result = await handler(request);
expect(result.status).toBe(500);
expect((await result.json()).message).toBe("Unexpected Server Error");
expect(result.headers.get("X-Remix-Error")).toBe("yes");
expect(headers.has("X-Remix-Response")).toBe(true);
expect(rootLoader.mock.calls.length).toBe(1);
expect(testAction.mock.calls.length).toBe(0);
});
Expand Down Expand Up @@ -704,7 +704,7 @@ describe("shared server runtime", () => {
let result = await handler(request);
expect(result.status).toBe(500);
expect((await result.json()).message).toBe(message);
expect(result.headers.get("X-Remix-Error")).toBe("yes");
expect(headers.has("X-Remix-Response")).toBe(true);
expect(rootLoader.mock.calls.length).toBe(1);
expect(testAction.mock.calls.length).toBe(0);
expect(spy.console.mock.calls.length).toBe(1);
Expand Down Expand Up @@ -737,7 +737,6 @@ describe("shared server runtime", () => {
let result = await handler(request);
expect(result.status).toBe(400);
expect(await result.text()).toBe("test");
expect(result.headers.get("X-Remix-Catch")).toBe("yes");
expect(rootLoader.mock.calls.length).toBe(1);
expect(testAction.mock.calls.length).toBe(0);
});
Expand Down Expand Up @@ -800,7 +799,7 @@ describe("shared server runtime", () => {
let result = await handler(request);
expect(result.status).toBe(500);
expect((await result.json()).message).toBe("Unexpected Server Error");
expect(result.headers.get("X-Remix-Error")).toBe("yes");
expect(headers.has("X-Remix-Response")).toBe(true);
expect(rootLoader.mock.calls.length).toBe(0);
expect(testAction.mock.calls.length).toBe(1);
});
Expand Down Expand Up @@ -834,7 +833,7 @@ describe("shared server runtime", () => {
let result = await handler(request);
expect(result.status).toBe(500);
expect((await result.json()).message).toBe(message);
expect(result.headers.get("X-Remix-Error")).toBe("yes");
expect(headers.has("X-Remix-Response")).toBe(true);
expect(rootLoader.mock.calls.length).toBe(0);
expect(testAction.mock.calls.length).toBe(1);
expect(spy.console.mock.calls.length).toBe(1);
Expand Down Expand Up @@ -867,7 +866,6 @@ describe("shared server runtime", () => {
let result = await handler(request);
expect(result.status).toBe(400);
expect(await result.text()).toBe("test");
expect(result.headers.get("X-Remix-Catch")).toBe("yes");
expect(rootLoader.mock.calls.length).toBe(0);
expect(testAction.mock.calls.length).toBe(1);
});
Expand Down
12 changes: 8 additions & 4 deletions packages/react-router/lib/dom/ssr/single-fetch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -610,10 +610,14 @@ async function fetchAndDecodeViaTurboStream(

let res = await fetch(url, await createRequestInit(request));

// If this 404'd without hitting the running server (most likely in a
// pre-rendered app using a CDN), then bubble a standard 404 ErrorResponse
if (res.status === 404 && !res.headers.has("X-Remix-Response")) {
throw new ErrorResponseImpl(404, "Not Found", true);
// If this error'd without hitting the running server, then bubble a normal
// `ErrorResponse` and don't try to decode the body with `turbo-stream`.
//
// This could be triggered by a few scenarios:
// - `.data` request 404 on a pre-rendered app using a CDN
// - 429 error returned from a CDN on a SSR app
if (res.status >= 400 && !res.headers.has("X-Remix-Response")) {
throw new ErrorResponseImpl(res.status, res.statusText, await res.text());
}

// Handle non-RR redirects (i.e., from express middleware)
Expand Down
12 changes: 8 additions & 4 deletions packages/react-router/lib/rsc/browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -529,10 +529,14 @@ function getFetchAndDecodeViaRSC(
new Request(url, await createRequestInit(request)),
);

// If this 404'd without hitting the running server (most likely in a
// pre-rendered app using a CDN), then bubble a standard 404 ErrorResponse
if (res.status === 404 && !res.headers.has("X-Remix-Response")) {
throw new ErrorResponseImpl(404, "Not Found", true);
// If this error'd without hitting the running server, then bubble a normal
// `ErrorResponse` and don't try to decode the body with `turbo-stream`.
//
// This could be triggered by a few scenarios:
// - `.data` request 404 on a pre-rendered app using a CDN
// - 429 error returned from a CDN on a SSR app
if (res.status >= 400 && !res.headers.has("X-Remix-Response")) {
throw new ErrorResponseImpl(res.status, res.statusText, await res.text());
}

invariant(res.body, "No response body to decode");
Expand Down
4 changes: 2 additions & 2 deletions packages/react-router/lib/rsc/server.rsc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,8 +455,8 @@ export async function matchRSCServerRequest({
generateResponse,
temporaryReferences,
);
// The front end uses this to know whether a 404 status came from app code
// or 404'd and never reached the origin server
// The front end uses this to know whether a 4xx/5xx status came from app code
// or never reached the origin server
response.headers.set("X-Remix-Response", "yes");
return response;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/react-router/lib/rsc/server.ssr.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ export async function routeRSCServerRequest({
headers.delete("Content-Encoding");
headers.delete("Content-Length");
headers.delete("Content-Type");
headers.delete("x-remix-response");
headers.delete("X-Remix-Response");
headers.set("Location", payload.location);

return new Response(serverResponseB?.body || "", {
Expand Down
6 changes: 0 additions & 6 deletions packages/react-router/lib/server-runtime/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -661,9 +661,6 @@ async function handleResourceRequest(

function handleQueryRouteError(error: unknown) {
if (isResponse(error)) {
// Note: Not functionally required but ensures that our response headers
// match identically to what Remix returns
error.headers.set("X-Remix-Catch", "yes");
return error;
}

Expand Down Expand Up @@ -701,9 +698,6 @@ function errorResponseToJson(
{
status: errorResponse.status,
statusText: errorResponse.statusText,
headers: {
"X-Remix-Error": "yes",
},
},
);
}
Expand Down
Loading