diff --git a/.changeset/rare-dodos-push.md b/.changeset/rare-dodos-push.md
new file mode 100644
index 00000000000..f8154384efa
--- /dev/null
+++ b/.changeset/rare-dodos-push.md
@@ -0,0 +1,5 @@
+---
+"@remix-run/server-runtime": patch
+---
+
+Don't log thrown response stubs via `handleError` in Single Fetch
diff --git a/integration/single-fetch-test.ts b/integration/single-fetch-test.ts
index 7b1abd76f95..ad0a3cf4fac 100644
--- a/integration/single-fetch-test.ts
+++ b/integration/single-fetch-test.ts
@@ -1667,6 +1667,81 @@ test.describe("single-fetch", () => {
);
});
+ test("does not log thrown redirect response stubs via handleError", async () => {
+ let fixture = await createFixture({
+ config: {
+ future: {
+ unstable_singleFetch: true,
+ },
+ },
+ files: {
+ ...files,
+ "app/routes/redirect.tsx": js`
+ export function action({ response }) {
+ response.status = 301;
+ response.headers.set("Location", "/data");
+ throw response;
+ }
+ export function loader({ response }) {
+ response.status = 301;
+ response.headers.set("Location", "/data");
+ throw response;
+ }
+ export default function Component() {
+ return
Should not see me
;
+ }
+ `,
+ },
+ });
+
+ let errorLogs = [];
+ console.error = (e) => errorLogs.push(e);
+ await fixture.requestDocument("/redirect");
+ await fixture.requestSingleFetchData("/redirect.data");
+ await fixture.requestSingleFetchData("/redirect.data", {
+ method: "post",
+ body: null,
+ });
+ expect(errorLogs.length).toBe(0);
+ });
+
+ test("does not log thrown non-redirect response stubs via handleError", async () => {
+ let fixture = await createFixture({
+ config: {
+ future: {
+ unstable_singleFetch: true,
+ },
+ },
+ files: {
+ ...files,
+ "app/routes/redirect.tsx": js`
+ export function action({ response }) {
+ response.status = 400;
+ throw response;
+ }
+ export function loader({ response }) {
+ response.status = 400;
+ throw response;
+ }
+ export default function Component() {
+ return Should not see me
;
+ }
+ `,
+ },
+ });
+
+ let errorLogs = [];
+ console.error = (e) => errorLogs.push(e);
+ await fixture.requestDocument("/redirect");
+ expect(errorLogs.length).toBe(1); // ErrorBoundary render logs this
+ await fixture.requestSingleFetchData("/redirect.data");
+ await fixture.requestSingleFetchData("/redirect.data", {
+ method: "post",
+ body: null,
+ });
+ expect(errorLogs.length).toBe(1);
+ });
+
test.describe("client loaders", () => {
test("when no routes have client loaders", async ({ page }) => {
let fixture = await createFixture(
diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts
index 11305551c9c..e73f7308fb6 100644
--- a/packages/remix-server-runtime/server.ts
+++ b/packages/remix-server-runtime/server.ts
@@ -40,6 +40,7 @@ import {
getSingleFetchDataStrategy,
getSingleFetchRedirect,
getSingleFetchResourceRouteDataStrategy,
+ isResponseStub,
mergeResponseStubs,
singleFetchAction,
singleFetchLoaders,
@@ -408,17 +409,6 @@ async function handleDocumentRequest(
return context;
}
- // Sanitize errors outside of development environments
- if (context.errors) {
- Object.values(context.errors).forEach((err) => {
- // @ts-expect-error This is "private" from users but intended for internal use
- if (!isRouteErrorResponse(err) || err.error) {
- handleError(err);
- }
- });
- context.errors = sanitizeErrors(context.errors, serverMode);
- }
-
let statusCode: number;
let headers: Headers;
if (build.future.unstable_singleFetch) {
@@ -437,6 +427,17 @@ async function handleDocumentRequest(
headers = getDocumentHeaders(build, context);
}
+ // Sanitize errors outside of development environments
+ if (context.errors) {
+ Object.values(context.errors).forEach((err) => {
+ // @ts-expect-error `err.error` is "private" from users but intended for internal use
+ if ((!isRouteErrorResponse(err) || err.error) && !isResponseStub(err)) {
+ handleError(err);
+ }
+ });
+ context.errors = sanitizeErrors(context.errors, serverMode);
+ }
+
// Server UI state to send to the client.
// - When single fetch is enabled, this is streamed down via `serverHandoffStream`
// - Otherwise it's stringified into `serverHandoffString`
diff --git a/packages/remix-server-runtime/single-fetch.ts b/packages/remix-server-runtime/single-fetch.ts
index bdfe745f7fc..c9303e79be0 100644
--- a/packages/remix-server-runtime/single-fetch.ts
+++ b/packages/remix-server-runtime/single-fetch.ts
@@ -184,7 +184,7 @@ export async function singleFetchAction(
if (context.errors) {
Object.values(context.errors).forEach((err) => {
// @ts-expect-error This is "private" from users but intended for internal use
- if (!isRouteErrorResponse(err) || err.error) {
+ if ((!isRouteErrorResponse(err) || err.error) && !isResponseStub(err)) {
handleError(err);
}
});
@@ -273,7 +273,7 @@ export async function singleFetchLoaders(
if (context.errors) {
Object.values(context.errors).forEach((err) => {
// @ts-expect-error This is "private" from users but intended for internal use
- if (!isRouteErrorResponse(err) || err.error) {
+ if ((!isRouteErrorResponse(err) || err.error) && !isResponseStub(err)) {
handleError(err);
}
});