diff --git a/.changeset/smart-mirrors-flow.md b/.changeset/smart-mirrors-flow.md new file mode 100644 index 00000000000..ffd88d253af --- /dev/null +++ b/.changeset/smart-mirrors-flow.md @@ -0,0 +1,6 @@ +--- +"@remix-run/server-runtime": patch +--- + +- Fix error when returning null from a resource route in single fetch +- Fix issues with returning or throwing a response stub from a resource route in single fetch diff --git a/integration/single-fetch-test.ts b/integration/single-fetch-test.ts index 75807384b80..a3aa4f0d039 100644 --- a/integration/single-fetch-test.ts +++ b/integration/single-fetch-test.ts @@ -1539,6 +1539,33 @@ test.describe("single-fetch", () => { console.warn = oldConsoleWarn; }); + test("allows resource routes to return null using a response stub", async () => { + let fixture = await createFixture( + { + config: { + future: { + unstable_singleFetch: true, + }, + }, + files: { + ...files, + "app/routes/resource.tsx": js` + export function loader({ response }) { + response.status = 201; + response.headers.set('X-Created-Id', '1'); + return null; + } + `, + }, + }, + ServerMode.Development + ); + let res = await fixture.requestResource("/resource"); + expect(res.status).toBe(201); + expect(res.headers.get("X-Created-Id")).toBe("1"); + expect(await res.text()).toBe(""); + }); + test("processes response stub onto resource routes returning raw data", async () => { let fixture = await createFixture( { @@ -1618,6 +1645,90 @@ test.describe("single-fetch", () => { }); }); + test("processes returned response stub redirects", async () => { + let fixture = await createFixture( + { + config: { + future: { + unstable_singleFetch: true, + }, + }, + files: { + ...files, + "app/routes/resource.tsx": js` + import { json } from '@remix-run/node'; + + export function loader({ response }) { + response.status = 301; + response.headers.set('Location', '/whatever') + return response; + } + `, + }, + }, + ServerMode.Development + ); + let res = await fixture.requestResource("/resource"); + expect(res.status).toBe(301); + expect(res.headers.get("Location")).toBe("/whatever"); + }); + + test("processes thrown response stub redirects", async () => { + let fixture = await createFixture( + { + config: { + future: { + unstable_singleFetch: true, + }, + }, + files: { + ...files, + "app/routes/resource.tsx": js` + import { json } from '@remix-run/node'; + + export function loader({ response }) { + response.status = 301; + response.headers.set('Location', '/whatever') + throw response; + } + `, + }, + }, + ServerMode.Development + ); + let res = await fixture.requestResource("/resource"); + expect(res.status).toBe(301); + expect(res.headers.get("Location")).toBe("/whatever"); + }); + + test("processes response stub redirects when null is returned", async () => { + let fixture = await createFixture( + { + config: { + future: { + unstable_singleFetch: true, + }, + }, + files: { + ...files, + "app/routes/resource.tsx": js` + import { json } from '@remix-run/node'; + + export function loader({ response }) { + response.status = 301; + response.headers.set('Location', '/whatever') + return null; + } + `, + }, + }, + ServerMode.Development + ); + let res = await fixture.requestResource("/resource"); + expect(res.status).toBe(301); + expect(res.headers.get("Location")).toBe("/whatever"); + }); + test("allows fetcher to hit resource route and return via turbo stream", async ({ page, }) => { diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts index 8c0a797a86a..566c515ecfc 100644 --- a/packages/remix-server-runtime/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -591,7 +591,7 @@ async function handleResourceRequest( : null), }); - if (typeof response === "object") { + if (typeof response === "object" && response !== null) { invariant( !(DEFERRED_SYMBOL in response), `You cannot return a \`defer()\` response from a Resource Route. Did you ` + @@ -609,6 +609,13 @@ async function handleResourceRequest( // @ts-expect-error response.headers[op](...args); } + } else if (isResponseStub(response) || response == null) { + // If the stub or null was returned, then there is no body so we just + // proxy along the status/headers to a Response + response = new Response(null, { + status: stub.status, + headers: stub.headers, + }); } else { console.warn( resourceRouteJsonWarning( @@ -639,6 +646,13 @@ async function handleResourceRequest( return error; } + if (isResponseStub(error)) { + return new Response(null, { + status: error.status, + headers: error.headers, + }); + } + if (isRouteErrorResponse(error)) { if (error) { handleError(error);