Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: remove v1 headers behavior #6979

Merged
merged 4 commits into from
Jul 31, 2023
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
8 changes: 8 additions & 0 deletions .changeset/v2-use-deepest-headers-function.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@remix-run/dev": major
"@remix-run/react": major
"@remix-run/server-runtime": major
"@remix-run/testing": major
---

Remove `v2_headers` flag. It is now the default behavior to use the deepest `headers` function in the route tree.
9 changes: 4 additions & 5 deletions docs/pages/api-development-strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,10 @@ The lifecycle is thus either:

## Current Future Flags

| Flag | Description |
| ------------ | --------------------------------------------------------------- |
| `v2_dev` | Enable the new development server (including HMR/HDR support) |
| `v2_headers` | Leverage ancestor `headers` if children do not export `headers` |
| `v2_meta` | Enable the new API for your `meta` functions |
| Flag | Description |
| --------- | ------------------------------------------------------------- |
| `v2_dev` | Enable the new development server (including HMR/HDR support) |
| `v2_meta` | Enable the new API for your `meta` functions |

[future-flags-blog-post]: https://remix.run/blog/future-flags
[feature-flowchart]: /docs-images/feature-flowchart.png
27 changes: 3 additions & 24 deletions docs/route/headers.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ If an action or a loader threw a `Response` and we're rendering a boundary, any

## Nested Routes

Because Remix has nested routes, there's a battle of the headers to be won when nested routes match. The default behavior is that Remix only leverages the resulting headers from the leaf rendered route. Consider these files in the routes directory:
Because Remix has nested routes, there's a battle of the headers to be won when nested routes match. The default behavior is that Remix only leverages the resulting headers from the deepest `headers` function it finds in the renderable matches (up to and including the boundary route if an error is present).

```
├── users.tsx
Expand All @@ -58,11 +58,9 @@ If we are looking at `/users/123/profile` then three routes are rendering:
</Users>
```

If all three define `headers`, the deepest module wins, in this case `profile.tsx`. However, if your `profile.tsx` loader threw and bubbled to a boundary in `userId.tsx` - then `userId.tsx`'s `headers` function would be used as it is the leaf rendered route.
If a user is looking at `/users/123/profile` and `profile.tsx` does not export a `headers` function, then Remix will use the return value of `$userId.tsx`'s `headers` function. If that file doesn't export one, then it will use the result of the one in `users.tsx`, and so on.

<docs-info>
We realize that it can be tedious and error-prone to have to define `headers` on every possible leaf route so we're changing the current behavior in v2 behind the [`future.v2_headers`][v2_headers] flag.
</docs-info>
If all three define `headers`, the deepest module wins, in this case `profile.tsx`. However, if your `profile.tsx` loader threw and bubbled to a boundary in `userId.tsx` - then `userId.tsx`'s `headers` function would be used as it is the leaf rendered route.

We don't want surprise headers in your responses, so it's your job to merge them if you'd like. Remix passes in the `parentHeaders` to your `headers` function. So `users.tsx` headers get passed to `$userId.tsx`, and then `$userId.tsx` headers are passed to `profile.tsx` headers.

Expand Down Expand Up @@ -131,25 +129,6 @@ export default function handleRequest(

Just keep in mind that doing this will apply to _all_ document requests, but does not apply to `data` requests (for client-side transitions for example). For those, use [`handleDataRequest`][handledatarequest].

## v2 Behavior

Since it can be tedious and error-prone to define a `header` function in every single possible leaf route, we're changing the behavior slightly in v2 and you can opt-into the new behavior via the `future.v2_headers` [Future Flag][future-flags] in `remix.config.js`.

When enabling this flag, Remix will now use the deepest `headers` function it finds in the renderable matches (up to and including the boundary route if an error is present). You'll still need to handle merging together headers as shown above for any `headers` functions above this route.

This means that, re-using the example above:

```
├── users.tsx
└── users
├── $userId.tsx
└── $userId
└── profile.tsx
```

If a user is looking at `/users/123/profile` and `profile.tsx` does not export a `headers` function, then Remix will use the return value of `$userId.tsx`'s `headers` function. If that file doesn't export one, then it will use the result of the one in `users.tsx`, and so on.

[headers]: https://developer.mozilla.org/en-US/docs/Web/API/Headers
[handledatarequest]: ../file-conventions/entry.server
[v2_headers]: #v2-behavior
[future-flags]: ../pages/api-development-strategy
156 changes: 0 additions & 156 deletions integration/headers-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,6 @@ test.describe("headers export", () => {
test.beforeAll(async () => {
appFixture = await createFixture(
{
config: {
future: {
v2_headers: true,
},
},
files: {
"app/root.jsx": js`
import { json } from "@remix-run/node";
Expand Down Expand Up @@ -424,154 +419,3 @@ test.describe("headers export", () => {
);
});
});

test.describe("v1 behavior (future.v2_headers=false)", () => {
let appFixture: Fixture;

test.beforeAll(async () => {
appFixture = await createFixture(
{
config: {
future: {
v2_headers: false,
},
},
files: {
"app/root.jsx": js`
import { json } from "@remix-run/node";
import { Links, Meta, Outlet, Scripts } from "@remix-run/react";

export const loader = () => json({});

export default function Root() {
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<Outlet />
<Scripts />
</body>
</html>
);
}
`,

"app/routes/parent.jsx": js`
export function headers({ actionHeaders, errorHeaders, loaderHeaders, parentHeaders }) {
return new Headers([
...(parentHeaders ? Array.from(parentHeaders.entries()) : []),
...(actionHeaders ? Array.from(actionHeaders.entries()) : []),
...(loaderHeaders ? Array.from(loaderHeaders.entries()) : []),
...(errorHeaders ? Array.from(errorHeaders.entries()) : []),
]);
}

export function loader({ request }) {
return new Response(null, {
headers: { 'X-Parent-Loader': 'success' },
})
}

export default function Component() { return <div/> }
`,

"app/routes/parent.child.jsx": js`
export async function action({ request }) {
return null;
}

export default function Component() { return <div/> }
`,

"app/routes/cookie.jsx": js`
import { json } from "@remix-run/server-runtime";
import { Outlet } from "@remix-run/react";

export function loader({ request }) {
if (new URL(request.url).searchParams.has("parent-throw")) {
throw json(null, { headers: { "Set-Cookie": "parent-thrown-cookie=true" } });
}
return null
};

export default function Parent() {
return <Outlet />;
}

export function ErrorBoundary() {
return <h1>Caught!</h1>;
}
`,

"app/routes/cookie.child.jsx": js`
import { json } from "@remix-run/node";

export function loader({ request }) {
if (new URL(request.url).searchParams.has("throw")) {
throw json(null, { headers: { "Set-Cookie": "thrown-cookie=true" } });
}
return json(null, {
headers: { "Set-Cookie": "normal-cookie=true" },
});
};

export default function Child() {
return <p>Child</p>;
}
`,
},
},
ServerMode.Test
);
});

test("returns no headers when the leaf route doesn't export a header function (GET)", async () => {
let response = await appFixture.requestDocument("/parent/child");
expect(JSON.stringify(Array.from(response.headers.entries()))).toBe(
JSON.stringify([["content-type", "text/html"]])
);
});

test("returns no headers when the leaf route doesn't export a header function (POST)", async () => {
let response = await appFixture.postDocument(
"/parent/child",
new URLSearchParams()
);
expect(JSON.stringify(Array.from(response.headers.entries()))).toBe(
JSON.stringify([["content-type", "text/html"]])
);
});

test("automatically includes cookie headers from normal responses", async () => {
let response = await appFixture.requestDocument("/cookie/child");
expect(JSON.stringify(Array.from(response.headers.entries()))).toBe(
JSON.stringify([
["content-type", "text/html"],
["set-cookie", "normal-cookie=true"],
])
);
});

test("automatically includes cookie headers from thrown responses", async () => {
let response = await appFixture.requestDocument("/cookie/child?throw");
expect(JSON.stringify(Array.from(response.headers.entries()))).toBe(
JSON.stringify([
["content-type", "text/html"],
["set-cookie", "thrown-cookie=true"],
])
);
});

test("does not duplicate thrown cookie headers from boundary route", async () => {
let response = await appFixture.requestDocument("/cookie?parent-throw");
expect(JSON.stringify(Array.from(response.headers.entries()))).toBe(
JSON.stringify([
["content-type", "text/html"],
["set-cookie", "parent-thrown-cookie=true"],
])
);
});
});
1 change: 0 additions & 1 deletion integration/hmr-log-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ let fixture = (options: { appPort: number; devPort: number }): FixtureInit => ({
port: options.devPort,
},
v2_meta: true,
v2_headers: true,
},
},
files: {
Expand Down
1 change: 0 additions & 1 deletion integration/hmr-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ let fixture = (options: { appPort: number; devPort: number }): FixtureInit => ({
port: options.devPort,
},
v2_meta: true,
v2_headers: true,
},
},
files: {
Expand Down
2 changes: 0 additions & 2 deletions packages/remix-dev/__tests__/readConfig-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ describe("readConfig", () => {
entryServerFilePath: expect.any(String),
tsconfigPath: expect.any(String),
future: {
v2_headers: expect.any(Boolean),
v2_meta: expect.any(Boolean),
},
},
Expand All @@ -42,7 +41,6 @@ describe("readConfig", () => {
"entryServerFilePath": Any<String>,
"future": Object {
"v2_dev": false,
"v2_headers": Any<Boolean>,
"v2_meta": Any<Boolean>,
},
"mdx": undefined,
Expand Down
12 changes: 0 additions & 12 deletions packages/remix-dev/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ type Dev = {

interface FutureConfig {
v2_dev: boolean | Dev;
v2_headers: boolean;
v2_meta: boolean;
}

Expand Down Expand Up @@ -399,10 +398,6 @@ export async function readConfig(
metaWarning();
}

if (!appConfig.future?.v2_headers) {
headersWarning();
}

let serverBuildPath = path.resolve(
rootDirectory,
appConfig.serverBuildPath ?? "build/index.js"
Expand Down Expand Up @@ -639,7 +634,6 @@ export async function readConfig(

let future: FutureConfig = {
v2_dev: appConfig.future?.v2_dev ?? false,
v2_headers: appConfig.future?.v2_headers === true,
v2_meta: appConfig.future?.v2_meta === true,
};

Expand Down Expand Up @@ -772,9 +766,3 @@ let metaWarning = futureFlagWarning({
flag: "v2_meta",
link: "https://remix.run/docs/en/v1.15.0/pages/v2#meta",
});

let headersWarning = futureFlagWarning({
message: "The route `headers` API is changing in v2",
flag: "v2_headers",
link: "https://remix.run/docs/en/v1.17.0/pages/v2#route-headers",
});
1 change: 0 additions & 1 deletion packages/remix-react/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ type Dev = {

export interface FutureConfig {
v2_dev: boolean | Dev;
v2_headers: boolean;
v2_meta: boolean;
}

Expand Down
1 change: 0 additions & 1 deletion packages/remix-server-runtime/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ type Dev = {

export interface FutureConfig {
v2_dev: boolean | Dev;
v2_headers: boolean;
v2_meta: boolean;
}

Expand Down
5 changes: 2 additions & 3 deletions packages/remix-server-runtime/headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,8 @@ export function getDocumentHeadersRR(
errorHeaders !== loaderHeaders &&
errorHeaders !== actionHeaders;

// When the future flag is enabled, use the parent headers for any route
// that doesn't have a `headers` export
if (routeModule.headers == null && build.future.v2_headers) {
// Use the parent headers for any route without a `headers` export
if (routeModule.headers == null) {
let headers = new Headers(parentHeaders);
if (includeErrorCookies) {
prependCookies(errorHeaders!, headers);
Expand Down
1 change: 0 additions & 1 deletion packages/remix-testing/create-remix-stub.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,6 @@ export function createRemixStub(
remixContextRef.current = {
future: {
v2_dev: false,
v2_headers: false,
v2_meta: false,
...remixConfigFuture,
},
Expand Down
1 change: 0 additions & 1 deletion templates/arc/remix.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export default {
future: {
v2_dev: true,

v2_headers: true,
v2_meta: true,
},
};
1 change: 0 additions & 1 deletion templates/cloudflare-pages/remix.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export default {
future: {
v2_dev: true,

v2_headers: true,
v2_meta: true,
},
};
1 change: 0 additions & 1 deletion templates/cloudflare-workers/remix.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export default {
future: {
v2_dev: true,

v2_headers: true,
v2_meta: true,
},
};
1 change: 0 additions & 1 deletion templates/deno/remix.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ module.exports = {
// serverBuildPath: "build/index.js",
// publicPath: "/build/",
future: {
v2_headers: true,
v2_meta: true,
},
};
1 change: 0 additions & 1 deletion templates/express/remix.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export default {
future: {
v2_dev: true,

v2_headers: true,
v2_meta: true,
},
};
1 change: 0 additions & 1 deletion templates/fly/remix.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ module.exports = {
future: {
v2_dev: true,

v2_headers: true,
v2_meta: true,
},
};
Loading