Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/sour-knives-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/start": patch
---

Update `h3` to `2.0.1-rc.26`.
5 changes: 5 additions & 0 deletions .changeset/spotty-jobs-relax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/start": patch
---

Fixed event response status and headers set during server-side rendering with deferred async resources, not being applied on the outgoing response.
11 changes: 5 additions & 6 deletions apps/tests/src/e2e/api-call.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,10 @@ test.describe("api calls", () => {
// 1. session=abc123 (from response headers)
// 2. csrf=xyz789 (from response headers)
// 3. event_cookie=from_event (from event.response headers via setHeader)
expect(cookies.length).toBe(3);

const cookieValues = cookies.join("; ");
expect(cookieValues).toContain("session=abc123");
expect(cookieValues).toContain("csrf=xyz789");
expect(cookieValues).toContain("event_cookie=from_event");
expect(cookies).toStrictEqual([
"session=abc123; Path=/; HttpOnly",
"csrf=xyz789; Path=/",
"event_cookie=from_event; Path=/",
]);
});
});
8 changes: 7 additions & 1 deletion apps/tests/src/e2e/http-header.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@ import { expect, test } from "@playwright/test";

test.describe("http header", () => {
// couldn't get this to see the headers but verified in chrome devtools
test.skip("should set http header", async ({ page }) => {
test("should set http header", async ({ page }) => {
const response = await page.goto("/http-header");

expect(response?.headers()["test-header"]).toBe("test-value");
});

test("should set http header with deferred data", async ({ page }) => {
const response = await page.goto("/http-header");

expect(response?.headers()["test-header-async"]).toBe("async-value");
});
});
13 changes: 13 additions & 0 deletions apps/tests/src/routes/http-header.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
import { createAsync, query } from "@solidjs/router";
import { HttpHeader } from "@solidjs/start";
import { Show } from "solid-js";
import { setTimeout } from "timers/promises";

const getData = query(async () => {
"use server";
await setTimeout(500);

return "async-value";
}, "http-header");

export default function HttpHeaderRoute() {
const data = createAsync(() => getData(), { deferStream: true });

return (
<main>
<h1>Http Header</h1>
<HttpHeader name="test-header" value="test-value" />
<Show when={data()}>{value => <HttpHeader name="test-header-async" value={value()} />}</Show>
</main>
);
}
2 changes: 1 addition & 1 deletion packages/start/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
"defu": "^6.1.4",
"error-stack-parser": "^2.1.4",
"fast-glob": "^3.3.3",
"h3": "^2.0.1-rc.25",
"h3": "^2.0.1-rc.26",
"html-to-image": "^1.11.13",
"micromatch": "^4.0.8",
"oxc-parser": "^0.139.0",
Expand Down
64 changes: 11 additions & 53 deletions packages/start/src/server/handler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import middleware from "solid-start:middleware";
import { defineHandler, getCookie, H3, type H3Event, redirect, setCookie } from "h3/generic";
import { join } from "pathe";
import {
defineHandler,
getCookie,
H3,
type H3Event,
iterable,
redirect,
setCookie,
} from "h3/generic";
import type { JSX } from "solid-js";
import { sharedConfig } from "solid-js";
import { getRequestEvent, renderToStream, renderToString } from "solid-js/web";
Expand Down Expand Up @@ -31,14 +38,7 @@ export function createBaseHandler(
const pathname = stripBaseUrl(url.pathname);

if (pathname.startsWith(SERVER_FN_BASE)) {
const serverFnResponse = await handleServerFunction(e);

if (serverFnResponse instanceof Response)
return produceResponseWithEventHeaders(serverFnResponse);

return new Response(serverFnResponse as any, {
headers: e.res.headers,
});
return await handleServerFunction(e);
}

const match = matchAPIRoute(pathname, event.request.method);
Expand All @@ -52,8 +52,6 @@ export function createBaseHandler(
sharedConfig.context = { event };
const res = await fn(event);
if (res !== undefined) {
if (res instanceof Response) return produceResponseWithEventHeaders(res);

return res;
}
if (event.request.method !== "GET") {
Expand Down Expand Up @@ -119,11 +117,9 @@ export function createBaseHandler(

if (mode === "async") return await stream;

delete (stream as any).then;

// h3 expects a standard web ReadableStream across runtimes. The adapter
// also tolerates cancellation while Solid finishes outstanding work.
return toWebReadableStream(stream);
return iterable(toWebReadableStream(stream));
}),
});

Expand Down Expand Up @@ -218,44 +214,6 @@ function handleStreamCompleteRedirect(context: PageEvent) {
};
}

function produceResponseWithEventHeaders(res: Response) {
const event = getRequestEvent()!;

let ret = res;

// Response.redirect returns an immutable value, so we clone on any redirect just in case
if (300 <= res.status && res.status < 400) {
const cookies = res.headers.getSetCookie?.() ?? [];
const headers = new Headers();
res.headers.forEach((value, key) => {
if (key.toLowerCase() !== "set-cookie") {
headers.set(key, value);
}
});
for (const cookie of cookies) {
headers.append("Set-Cookie", cookie);
}
ret = new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers,
});
}

const eventCookies = event.response.headers.getSetCookie?.() ?? [];
for (const cookie of eventCookies) {
ret.headers.append("Set-Cookie", cookie);
}

for (const [name, value] of event.response.headers) {
if (name.toLowerCase() !== "set-cookie") {
ret.headers.set(name, value);
}
}

return ret;
}

function stripBaseUrl(path: string) {
const base = import.meta.env.SERVER_BASE_URL || import.meta.env.BASE_URL || "/";
return stripPathBase(path, base);
Expand Down
10 changes: 7 additions & 3 deletions packages/start/src/server/web-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@ type PipeableStream = {
};

/** Convert Solid's streaming SSR result into a cancellation-safe web stream. */
export function toWebReadableStream(stream: PipeableStream): ReadableStream<Uint8Array> {
export function toWebReadableStream(stream: PipeableStream) {
const encoder = new TextEncoder();
let active = true;

return new ReadableStream({
return new ReadableStream<Uint8Array>({
start(controller) {
stream.pipe({
write(payload) {
if (active) controller.enqueue(encoder.encode(payload));
if (!active) return;

// Encoding string to Uint8Array makes sure that
// the stream can be consumed as Response body
controller.enqueue(encoder.encode(payload));
},
end() {
if (!active) return;
Expand Down
Loading
Loading