From 3523ae54594d41dcf61a59aa634ba3f6268df225 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 19 Mar 2024 17:19:12 -0400 Subject: [PATCH] Scope single fetch headers calls to only loaded routes (#9085) --- integration/single-fetch-test.ts | 110 ++++++++++++++++++++++- packages/remix-server-runtime/headers.ts | 9 +- packages/remix-server-runtime/server.ts | 6 +- 3 files changed, 118 insertions(+), 7 deletions(-) diff --git a/integration/single-fetch-test.ts b/integration/single-fetch-test.ts index 8646069c245..3c0736d8dfe 100644 --- a/integration/single-fetch-test.ts +++ b/integration/single-fetch-test.ts @@ -416,7 +416,7 @@ test.describe("single-fetch", () => { expect(urls).toEqual([]); }); - test("returns loader headers through the headers function", async () => { + test("handles headers correctly for loader and action calls", async () => { let fixture = await createFixture({ config: { future: { @@ -476,6 +476,114 @@ test.describe("single-fetch", () => { expect(res.headers.get("x-headers-function")).toEqual(null); }); + test("scopes loader headers to the _routes param if present", async () => { + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, + }, + }, + files: { + ...files, + "app/routes/a.tsx": js` + export function headers({ loaderHeaders }) { + let headers = new Headers(loaderHeaders); + headers.set('x-a-headers', 'true') + return headers; + } + + export function loader({ request }) { + return new Response(null, { headers: { "x-a-loader": "true" } }); + } + + export default function Comp() { + return null; + } + `, + "app/routes/a.b.tsx": js` + export function headers({ loaderHeaders, parentHeaders }) { + let headers = new Headers(parentHeaders); + loaderHeaders.forEach((value, name) => headers.set(name, value)); + headers.set('x-b-headers', 'true') + return headers; + } + + export function loader({ request }) { + return new Response(null, { headers: { "x-b-loader": "true" } }); + } + + export default function Comp() { + return null; + } + `, + "app/routes/a.b.c.tsx": js` + export function headers({ loaderHeaders, parentHeaders }) { + let headers = new Headers(parentHeaders); + loaderHeaders.forEach((value, name) => headers.set(name, value)); + headers.set('x-c-headers', 'true') + return headers; + } + + export function loader({ request }) { + return new Response(null, { headers: { "x-c-loader": "true" } }); + } + + export default function Comp() { + return null; + } + `, + }, + }); + + let res = await fixture.requestSingleFetchData("/a/b/c.data"); + expect(res.headers.get("x-a-loader")).toEqual("true"); + expect(res.headers.get("x-a-headers")).toEqual("true"); + expect(res.headers.get("x-b-loader")).toEqual("true"); + expect(res.headers.get("x-b-headers")).toEqual("true"); + expect(res.headers.get("x-c-loader")).toEqual("true"); + expect(res.headers.get("x-c-headers")).toEqual("true"); + + res = await fixture.requestSingleFetchData( + "/a/b/c.data?_routes=routes%2Fa,routes%2Fa.b" + ); + expect(res.headers.get("x-a-loader")).toEqual("true"); + expect(res.headers.get("x-a-headers")).toEqual("true"); + expect(res.headers.get("x-b-loader")).toEqual("true"); + expect(res.headers.get("x-b-headers")).toEqual("true"); + expect(res.headers.get("x-c-loader")).toBeNull(); + expect(res.headers.get("x-c-headers")).toBeNull(); + + res = await fixture.requestSingleFetchData( + "/a/b/c.data?_routes=routes%2Fa" + ); + expect(res.headers.get("x-a-loader")).toEqual("true"); + expect(res.headers.get("x-a-headers")).toEqual("true"); + expect(res.headers.get("x-b-loader")).toBeNull(); + expect(res.headers.get("x-b-headers")).toBeNull(); + expect(res.headers.get("x-c-loader")).toBeNull(); + expect(res.headers.get("x-c-headers")).toBeNull(); + + res = await fixture.requestSingleFetchData( + "/a/b/c.data?_routes=routes%2Fa.b.c" + ); + expect(res.headers.get("x-a-loader")).toBeNull(); + expect(res.headers.get("x-a-headers")).toBeNull(); + expect(res.headers.get("x-b-loader")).toBeNull(); + expect(res.headers.get("x-b-headers")).toBeNull(); + expect(res.headers.get("x-c-loader")).toEqual("true"); + expect(res.headers.get("x-c-headers")).toEqual("true"); + + res = await fixture.requestSingleFetchData( + "/a/b/c.data?_routes=routes%2Fa,routes%2Fa.b.c" + ); + expect(res.headers.get("x-a-loader")).toEqual("true"); + expect(res.headers.get("x-a-loader")).toEqual("true"); + expect(res.headers.get("x-b-headers")).toBeNull(); + expect(res.headers.get("x-b-headers")).toBeNull(); + expect(res.headers.get("x-c-loader")).toEqual("true"); + expect(res.headers.get("x-c-headers")).toEqual("true"); + }); + test.describe("client loaders", () => { test("when no routes have client loaders", async ({ page }) => { let fixture = await createFixture( diff --git a/packages/remix-server-runtime/headers.ts b/packages/remix-server-runtime/headers.ts index d65644a0551..698b6af1542 100644 --- a/packages/remix-server-runtime/headers.ts +++ b/packages/remix-server-runtime/headers.ts @@ -3,9 +3,10 @@ import { splitCookiesString } from "set-cookie-parser"; import type { ServerBuild } from "./build"; -export function getDocumentHeadersRR( +export function getDocumentHeaders( build: ServerBuild, - context: StaticHandlerContext + context: StaticHandlerContext, + loadRouteIds?: string[] ): Headers { let boundaryIdx = context.errors ? context.matches.findIndex((m) => context.errors![m.route.id]) @@ -15,6 +16,10 @@ export function getDocumentHeadersRR( ? context.matches.slice(0, boundaryIdx + 1) : context.matches; + if (loadRouteIds) { + matches = matches.filter((m) => loadRouteIds.includes(m.route.id)); + } + let errorHeaders: Headers | undefined; if (boundaryIdx >= 0) { diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts index e8199f1ffd5..b4f03e55640 100644 --- a/packages/remix-server-runtime/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -24,7 +24,7 @@ import { serializeError, serializeErrors, } from "./errors"; -import { getDocumentHeadersRR as getDocumentHeaders } from "./headers"; +import { getDocumentHeaders } from "./headers"; import invariant from "./invariant"; import { ServerMode, isServerMode } from "./mode"; import type { RouteMatch } from "./routeMatching"; @@ -353,7 +353,6 @@ async function handleSingleFetchRequest( request, handlerUrl, staticHandler, - matches, loadContext, handleError, serverMode, @@ -447,7 +446,6 @@ async function singleFetchLoaders( request: Request, handlerUrl: URL, staticHandler: StaticHandler, - matches: RouteMatch[] | null, loadContext: AppLoadContext, handleError: (err: unknown) => void, serverMode: ServerMode, @@ -509,7 +507,7 @@ async function singleFetchLoaders( return { result: results, - headers: getDocumentHeaders(build, context), + headers: getDocumentHeaders(build, context, loadRouteIds), status: context.statusCode, }; } catch (error: unknown) {