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

fix: handling http response headers #106

Merged
merged 3 commits into from
Dec 23, 2024
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
92 changes: 90 additions & 2 deletions docs/src/pages/en/(pages)/framework/http.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export default function MyComponent() {
## Response
</Link>

With `useResponse()` you can get access to the full HTTP response.
With `useResponse()` you can get access to the full HTTP response. This is only available after the response has been sent to the client, in a React component which was suspended and streamed to the client later than the response was sent.

```jsx
import { useResponse } from "@lazarv/react-server";
Expand Down Expand Up @@ -143,7 +143,7 @@ export default function MyComponent() {
};
```

You can also modify the headers of the current response.
You can also modify the headers of the current response by passing an object of key-value pairs:

```jsx
import { headers } from "@lazarv/react-server";
Expand All @@ -157,6 +157,50 @@ export default function MyComponent() {
};
```

Or by passing a `Headers` object:

```jsx
import { headers } from "@lazarv/react-server";

export default function MyComponent() {
headers(new Headers({
"X-My-Header": "My value",
}));

return <p>Headers: {JSON.stringify(headers())}</p>;
};
```

Or by passing an array of key-value pairs:

```jsx
import { headers } from "@lazarv/react-server";

export default function MyComponent() {
headers([
["X-My-Header", "My value"],
]);

return <p>Headers: {JSON.stringify(headers())}</p>;
};
```

Modifying the headers with the `headers()` function will override the headers of the current response. If you want to mutate the response headers directly, you can use three addition helper functions to set, append or delete headers. These functions are `setHeader()`, `appendHeader()` and `deleteHeader()`.

```jsx
import { setHeader, appendHeader, deleteHeader } from "@lazarv/react-server";

export default function MyComponent() {
setHeader("X-My-Header", "My first value");
appendHeader("X-My-Header", "My second value");
deleteHeader("X-My-Header");

return <p>Check the response headers!</p>;
}
```

> **Note:** Keep in mind that HTTP headers are case-insensitive!
<Link name="cookies">
## Cookies
</Link>
Expand Down Expand Up @@ -277,3 +321,47 @@ export default function MyComponent() {
return <p>Outlet: {outlet}</p>;
}
```

<Link name="render-lock">
## Render Lock
</Link>

With `useRender()` you can get access to the render lock of the current request. This is useful if you want to lock rendering of a React Server Component while the async function is running or until the lock is released, as React Server Components are rendered using streaming by default. This is especially useful for handling HTTP headers and cookies in an async React Server Component. Without locking the rendering, the headers and cookies will be sent to the client before the async function is finished. When a lock is detected in the rendering process, the rendering will wait for the lock to be released before beginning to send the headers and cookies to the client and starting the streaming of the React Server Component.

```jsx
import { headers, useRender } from "@lazarv/react-server";

export default function MyComponent() {
const { lock } = useRender();

await lock(async () => {
// Do something async
await new Promise((resolve) => setTimeout(resolve, 1000));
headers({
"x-lock": "works",
});
});

return <p>Render lock</p>;
}
```

You can also use the `lock()` function to get an `unlock()` function to release the lock later.

```jsx
import { headers, useRender } from "@lazarv/react-server";

export default function MyComponent() {
const { lock } = useRender();

const unlock = lock();
// Do something async
await new Promise((resolve) => setTimeout(resolve, 1000));
headers({
"x-lock": "works",
});
unlock();

return <p>Render lock</p>;
}
```
6 changes: 5 additions & 1 deletion packages/react-server/lib/build/server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,11 @@ export default async function serverBuild(root, options) {
entryFileNames: "[name].mjs",
chunkFileNames: "server/[name].[hash].mjs",
manualChunks: (id, ...rest) => {
if (id.includes("@lazarv/react-server") && id.endsWith(".mjs")) {
if (
(id.includes("@lazarv/react-server") ||
id.includes(sys.rootDir)) &&
id.endsWith(".mjs")
) {
return "@lazarv/react-server";
}
return (
Expand Down
3 changes: 3 additions & 0 deletions packages/react-server/lib/dev/ssr-handler.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
MEMORY_CACHE_CONTEXT,
MODULE_LOADER,
REDIRECT_CONTEXT,
RENDER,
RENDER_CONTEXT,
RENDER_STREAM,
SERVER_CONTEXT,
Expand Down Expand Up @@ -109,6 +110,7 @@ export default async function ssrHandler(root) {

const renderContext = createRenderContext(httpContext);
context$(RENDER_CONTEXT, renderContext);
context$(RENDER, render);

try {
const middlewares = await root_init$?.();
Expand All @@ -132,6 +134,7 @@ export default async function ssrHandler(root) {
}

const styles = collectStylesheets?.(rootModule) ?? [];
styles.unshift(...(getContext(STYLES_CONTEXT) ?? []));
context$(STYLES_CONTEXT, styles);

await module_loader_init$?.(ssrLoadModule, moduleCacheStorage);
Expand Down
16 changes: 8 additions & 8 deletions packages/react-server/lib/handlers/error.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,12 @@ function plainResponse(e) {
status: 500,
statusText: "Internal Server Error",
};
const headers = getContext(HTTP_HEADERS) ?? new Headers();
headers.set("Content-Type", "text/plain; charset=utf-8");

return new Response(e?.stack ?? null, {
...httpStatus,
headers: {
"Content-Type": "text/plain; charset=utf-8",
...(getContext(HTTP_HEADERS) ?? {}),
},
headers,
});
}

Expand All @@ -130,6 +130,9 @@ export default async function errorHandler(err) {
statusText: "Internal Server Error",
};

const headers = getContext(HTTP_HEADERS) ?? new Headers();
headers.set("Content-Type", "text/html; charset=utf-8");

const error = await prepareError(err);

const viteDevServer = getContext(SERVER_CONTEXT);
Expand Down Expand Up @@ -163,10 +166,7 @@ export default async function errorHandler(err) {
</html>`,
{
...httpStatus,
headers: {
"Content-Type": "text/html; charset=utf-8",
...(getContext(HTTP_HEADERS) ?? {}),
},
headers,
}
);
} catch (e) {
Expand Down
100 changes: 56 additions & 44 deletions packages/react-server/lib/plugins/file-router/entrypoint.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import {
routes,
} from "@lazarv/react-server/file-router/manifest";
import { useMatch } from "@lazarv/react-server/router";
import { context$ } from "@lazarv/react-server/server/context.mjs";
import { context$, getContext } from "@lazarv/react-server/server/context.mjs";
import { ROUTE_MATCH } from "@lazarv/react-server/server/symbols.mjs";

const PAGE_MATCH = Symbol("PAGE_MATCH");
const PAGE_COMPONENT = Symbol("PAGE_COMPONENT");

export async function init$() {
return async (context) => {
for (const handler of middlewares) {
Expand Down Expand Up @@ -43,58 +46,67 @@ export async function init$() {
(() => {})
)(context);
}
};
}

export default async function App() {
let match = null;
let Page = () => {
status(404);
return null;
};

const reactServerOutlet = useOutlet();
if (reactServerOutlet && reactServerOutlet !== "PAGE_ROOT") {
const outlets = pages.filter(
([, type, outlet]) => type === "page" && outlet === reactServerOutlet
);
for (const [path, , , , , lazy] of outlets) {
const match = useMatch(path, { exact: true });
if (match) {
const { default: Component, init$: page_init$ } = await lazy();
await page_init$?.();
return <Component {...match} />;
const reactServerOutlet = useOutlet();
if (reactServerOutlet && reactServerOutlet !== "PAGE_ROOT") {
const outlets = pages.filter(
([, type, outlet]) => type === "page" && outlet === reactServerOutlet
);
for (const [path, , , , , lazy] of outlets) {
const match = useMatch(path, { exact: true });
if (match) {
const { default: Component, init$: page_init$ } = await lazy();
await page_init$?.();
context$(PAGE_COMPONENT, Component);
context$(PAGE_MATCH, match);
return;
}
}
}
return null;
}

for (const [path, type, outlet, lazy, src] of pages) {
match = type === "page" && !outlet ? useMatch(path, { exact: true }) : null;
if (match) {
const { default: Component, init$: page_init$ } = await lazy();
Page = Component;
await page_init$?.();
break;
context$(PAGE_COMPONENT, null);
return;
}

match = type === "page" && outlet ? useMatch(path, { exact: true }) : null;
if (match) {
const [, , , lazy] =
pages.find(
([, type, outlet, , pageSrc]) =>
type === "page" &&
!outlet &&
dirname(src).includes(dirname(pageSrc))
) ?? [];
if (lazy) {
for (const [path, type, outlet, lazy, src] of pages) {
match =
type === "page" && !outlet ? useMatch(path, { exact: true }) : null;
if (match) {
const { default: Component, init$: page_init$ } = await lazy();
Page = Component;
await page_init$?.();
context$(PAGE_COMPONENT, Component);
context$(PAGE_MATCH, match);
break;
}

match =
type === "page" && outlet ? useMatch(path, { exact: true }) : null;
if (match) {
const [, , , lazy] =
pages.find(
([, type, outlet, , pageSrc]) =>
type === "page" &&
!outlet &&
dirname(src).includes(dirname(pageSrc))
) ?? [];
if (lazy) {
const { default: Component, init$: page_init$ } = await lazy();
await page_init$?.();
context$(PAGE_COMPONENT, Component);
context$(PAGE_MATCH, match);
break;
}
}
}
}
};
}

export default async function App() {
let match = getContext(PAGE_MATCH) ?? null;
let Page =
getContext(PAGE_COMPONENT) ??
(() => {
status(404);
return null;
});

return <Page {...match} />;
}
11 changes: 7 additions & 4 deletions packages/react-server/lib/start/ssr-handler.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
POSTPONE_STATE,
PRELUDE_HTML,
REDIRECT_CONTEXT,
RENDER,
RENDER_CONTEXT,
RENDER_STREAM,
SERVER_CONTEXT,
Expand Down Expand Up @@ -126,12 +127,13 @@ export default async function ssrHandler(root, options = {}) {
status: 500,
statusText: "Internal Server Error",
};

const headers = getContext(HTTP_HEADERS) ?? new Headers();
headers.set("Content-Type", "text/plain; charset=utf-8");

return new Response(e?.stack ?? null, {
...httpStatus,
headers: {
"Content-Type": "text/plain; charset=utf-8",
...(getContext(HTTP_HEADERS) ?? {}),
},
headers,
});
};

Expand Down Expand Up @@ -176,6 +178,7 @@ export default async function ssrHandler(root, options = {}) {

const renderContext = createRenderContext(httpContext);
context$(RENDER_CONTEXT, renderContext);
context$(RENDER, render);

try {
const middlewares = await root_init$?.();
Expand Down
Loading
Loading