Skip to content

Commit eb61a29

Browse files
authored
Avoid decoding CDN errors that never reached the origin server (#14385)
1 parent eebd2e8 commit eb61a29

File tree

9 files changed

+38
-31
lines changed

9 files changed

+38
-31
lines changed

.changeset/happy-hornets-judge.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
Do not try to use `turbo-stream` to decode CDN errors that never reached the server
6+
7+
- We used to do this but lost this check with the adoption of single fetch

integration/error-boundary-v2-test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,8 @@ test.describe("ErrorBoundary", () => {
177177
await waitForAndAssert(
178178
page,
179179
app,
180-
"#parent-error",
181-
"Unable to decode turbo-stream response",
180+
"#parent-error-response",
181+
"500 CDN Error!",
182182
);
183183
});
184184
});

integration/error-data-request-test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ test.describe("ErrorBoundary", () => {
110110
let { status, headers, data } =
111111
await fixture.requestSingleFetchData("/_root.data");
112112
expect(status).toBe(200);
113-
expect(headers.has("X-Remix-Error")).toBe(false);
113+
expect(headers.has("X-Remix-Response")).toBe(true);
114114
expect(data).toEqual({});
115115
});
116116

@@ -122,7 +122,7 @@ test.describe("ErrorBoundary", () => {
122122
},
123123
);
124124
expect(status).toBe(405);
125-
expect(headers.has("X-Remix-Error")).toBe(false);
125+
expect(headers.has("X-Remix-Response")).toBe(true);
126126
expect(data).toEqual({
127127
error: new ErrorResponseImpl(
128128
405,
@@ -153,7 +153,7 @@ test.describe("ErrorBoundary", () => {
153153
"/i/match/nothing.data",
154154
);
155155
expect(status).toBe(404);
156-
expect(headers.has("X-Remix-Error")).toBe(false);
156+
expect(headers.has("X-Remix-Response")).toBe(true);
157157
expect(data).toEqual({
158158
root: {
159159
error: new ErrorResponseImpl(

packages/react-router/__tests__/server-runtime/server-test.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -555,7 +555,7 @@ describe("shared server runtime", () => {
555555

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

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

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

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

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

@@ -670,7 +670,7 @@ describe("shared server runtime", () => {
670670
let result = await handler(request);
671671
expect(result.status).toBe(500);
672672
expect((await result.json()).message).toBe("Unexpected Server Error");
673-
expect(result.headers.get("X-Remix-Error")).toBe("yes");
673+
expect(headers.has("X-Remix-Response")).toBe(true);
674674
expect(rootLoader.mock.calls.length).toBe(1);
675675
expect(testAction.mock.calls.length).toBe(0);
676676
});
@@ -704,7 +704,7 @@ describe("shared server runtime", () => {
704704
let result = await handler(request);
705705
expect(result.status).toBe(500);
706706
expect((await result.json()).message).toBe(message);
707-
expect(result.headers.get("X-Remix-Error")).toBe("yes");
707+
expect(headers.has("X-Remix-Response")).toBe(true);
708708
expect(rootLoader.mock.calls.length).toBe(1);
709709
expect(testAction.mock.calls.length).toBe(0);
710710
expect(spy.console.mock.calls.length).toBe(1);
@@ -737,7 +737,6 @@ describe("shared server runtime", () => {
737737
let result = await handler(request);
738738
expect(result.status).toBe(400);
739739
expect(await result.text()).toBe("test");
740-
expect(result.headers.get("X-Remix-Catch")).toBe("yes");
741740
expect(rootLoader.mock.calls.length).toBe(1);
742741
expect(testAction.mock.calls.length).toBe(0);
743742
});
@@ -800,7 +799,7 @@ describe("shared server runtime", () => {
800799
let result = await handler(request);
801800
expect(result.status).toBe(500);
802801
expect((await result.json()).message).toBe("Unexpected Server Error");
803-
expect(result.headers.get("X-Remix-Error")).toBe("yes");
802+
expect(headers.has("X-Remix-Response")).toBe(true);
804803
expect(rootLoader.mock.calls.length).toBe(0);
805804
expect(testAction.mock.calls.length).toBe(1);
806805
});
@@ -834,7 +833,7 @@ describe("shared server runtime", () => {
834833
let result = await handler(request);
835834
expect(result.status).toBe(500);
836835
expect((await result.json()).message).toBe(message);
837-
expect(result.headers.get("X-Remix-Error")).toBe("yes");
836+
expect(headers.has("X-Remix-Response")).toBe(true);
838837
expect(rootLoader.mock.calls.length).toBe(0);
839838
expect(testAction.mock.calls.length).toBe(1);
840839
expect(spy.console.mock.calls.length).toBe(1);
@@ -867,7 +866,6 @@ describe("shared server runtime", () => {
867866
let result = await handler(request);
868867
expect(result.status).toBe(400);
869868
expect(await result.text()).toBe("test");
870-
expect(result.headers.get("X-Remix-Catch")).toBe("yes");
871869
expect(rootLoader.mock.calls.length).toBe(0);
872870
expect(testAction.mock.calls.length).toBe(1);
873871
});

packages/react-router/lib/dom/ssr/single-fetch.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -610,10 +610,14 @@ async function fetchAndDecodeViaTurboStream(
610610

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

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

619623
// Handle non-RR redirects (i.e., from express middleware)

packages/react-router/lib/rsc/browser.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -530,10 +530,14 @@ function getFetchAndDecodeViaRSC(
530530
new Request(url, await createRequestInit(request)),
531531
);
532532

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

539543
invariant(res.body, "No response body to decode");

packages/react-router/lib/rsc/server.rsc.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -455,8 +455,8 @@ export async function matchRSCServerRequest({
455455
generateResponse,
456456
temporaryReferences,
457457
);
458-
// The front end uses this to know whether a 404 status came from app code
459-
// or 404'd and never reached the origin server
458+
// The front end uses this to know whether a 4xx/5xx status came from app code
459+
// or never reached the origin server
460460
response.headers.set("X-Remix-Response", "yes");
461461
return response;
462462
}

packages/react-router/lib/rsc/server.ssr.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ export async function routeRSCServerRequest({
196196
headers.delete("Content-Encoding");
197197
headers.delete("Content-Length");
198198
headers.delete("Content-Type");
199-
headers.delete("x-remix-response");
199+
headers.delete("X-Remix-Response");
200200
headers.set("Location", payload.location);
201201

202202
return new Response(serverResponseB?.body || "", {

packages/react-router/lib/server-runtime/server.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -661,9 +661,6 @@ async function handleResourceRequest(
661661

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

@@ -701,9 +698,6 @@ function errorResponseToJson(
701698
{
702699
status: errorResponse.status,
703700
statusText: errorResponse.statusText,
704-
headers: {
705-
"X-Remix-Error": "yes",
706-
},
707701
},
708702
);
709703
}

0 commit comments

Comments
 (0)