diff --git a/packages/start/src/server/handler.ts b/packages/start/src/server/handler.ts index cdcacc600..0915b48bc 100644 --- a/packages/start/src/server/handler.ts +++ b/packages/start/src/server/handler.ts @@ -234,7 +234,7 @@ function produceResponseWithEventHeaders(res: Response) { 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) { + if((300 <= res.status && res.status < 400) || true) { ret = new Response(res.body, { status: res.status, statusText: res.statusText, diff --git a/packages/start/src/server/server-functions-handler.ts b/packages/start/src/server/server-functions-handler.ts index 2466567d0..cab182d3d 100644 --- a/packages/start/src/server/server-functions-handler.ts +++ b/packages/start/src/server/server-functions-handler.ts @@ -1,22 +1,18 @@ import { getServerFnById } from "solidstart:server-fn-manifest"; import { parseSetCookie } from "cookie-es"; import { type H3Event, parseCookies } from "h3"; +import { crossSerializeStream, fromJSON, getCrossReferenceHeader } from "seroval"; import { - crossSerializeStream, - fromJSON, - getCrossReferenceHeader, -} from "seroval"; -import { - CustomEventPlugin, - DOMExceptionPlugin, - EventPlugin, - FormDataPlugin, - HeadersPlugin, - ReadableStreamPlugin, - RequestPlugin, - ResponsePlugin, - URLPlugin, - URLSearchParamsPlugin, + CustomEventPlugin, + DOMExceptionPlugin, + EventPlugin, + FormDataPlugin, + HeadersPlugin, + ReadableStreamPlugin, + RequestPlugin, + ResponsePlugin, + URLPlugin, + URLSearchParamsPlugin } from "seroval-plugins/web"; import { sharedConfig } from "solid-js"; import { renderToString } from "solid-js/web"; @@ -28,337 +24,316 @@ import type { FetchEvent, PageEvent } from "./types.ts"; import { getExpectedRedirectStatus } from "./util.ts"; function createChunk(data: string) { - const encodeData = new TextEncoder().encode(data); - const bytes = encodeData.length; - const baseHex = bytes.toString(16); - const totalHex = "00000000".substring(0, 8 - baseHex.length) + baseHex; // 32-bit - const head = new TextEncoder().encode(`;0x${totalHex};`); + const encodeData = new TextEncoder().encode(data); + const bytes = encodeData.length; + const baseHex = bytes.toString(16); + const totalHex = "00000000".substring(0, 8 - baseHex.length) + baseHex; // 32-bit + const head = new TextEncoder().encode(`;0x${totalHex};`); - const chunk = new Uint8Array(12 + bytes); - chunk.set(head); - chunk.set(encodeData, 12); - return chunk; + const chunk = new Uint8Array(12 + bytes); + chunk.set(head); + chunk.set(encodeData, 12); + return chunk; } function serializeToStream(id: string, value: any) { - return new ReadableStream({ - start(controller) { - crossSerializeStream(value, { - scopeId: id, - plugins: [ - CustomEventPlugin, - DOMExceptionPlugin, - EventPlugin, - FormDataPlugin, - HeadersPlugin, - ReadableStreamPlugin, - RequestPlugin, - ResponsePlugin, - URLSearchParamsPlugin, - URLPlugin, - ], - onSerialize(data: string, initial: boolean) { - controller.enqueue( - createChunk( - initial ? `(${getCrossReferenceHeader(id)},${data})` : data, - ), - ); - }, - onDone() { - controller.close(); - }, - onError(error: any) { - controller.error(error); - }, - }); - }, - }); + return new ReadableStream({ + start(controller) { + crossSerializeStream(value, { + scopeId: id, + plugins: [ + CustomEventPlugin, + DOMExceptionPlugin, + EventPlugin, + FormDataPlugin, + HeadersPlugin, + ReadableStreamPlugin, + RequestPlugin, + ResponsePlugin, + URLSearchParamsPlugin, + URLPlugin + ], + onSerialize(data: string, initial: boolean) { + controller.enqueue( + createChunk(initial ? `(${getCrossReferenceHeader(id)},${data})` : data) + ); + }, + onDone() { + controller.close(); + }, + onError(error: any) { + controller.error(error); + } + }); + } + }); } export async function handleServerFunction(h3Event: H3Event) { - const event = getFetchEvent(h3Event); - const request = event.request; + const event = getFetchEvent(h3Event); + const request = event.request; - const serverReference = request.headers.get("X-Server-Id"); - const instance = request.headers.get("X-Server-Instance"); - const singleFlight = request.headers.has("X-Single-Flight"); - const url = new URL(request.url); - let functionId: string | undefined | null; - if (serverReference) { - // invariant(typeof serverReference === "string", "Invalid server function"); - [functionId] = serverReference.split("#"); - } else { - functionId = url.searchParams.get("id"); + const serverReference = request.headers.get("X-Server-Id"); + const instance = request.headers.get("X-Server-Instance"); + const singleFlight = request.headers.has("X-Single-Flight"); + const url = new URL(request.url); + let functionId: string | undefined | null; + if (serverReference) { + // invariant(typeof serverReference === "string", "Invalid server function"); + [functionId] = serverReference.split("#"); + } else { + functionId = url.searchParams.get("id"); - if (!functionId) { - return process.env.NODE_ENV === "development" - ? new Response("Server function not found", { status: 404 }) - : new Response(null, { status: 404 }); - } - } + if (!functionId) { + return process.env.NODE_ENV === "development" + ? new Response("Server function not found", { status: 404 }) + : new Response(null, { status: 404 }); + } + } - const serverFunction = await getServerFnById(functionId!); + const serverFunction = await getServerFnById(functionId!); - let parsed: any[] = []; + let parsed: any[] = []; - // grab bound arguments from url when no JS - if (!instance || h3Event.method === "GET") { - const args = url.searchParams.get("args"); - if (args) { - const json = JSON.parse(args); - (json.t - ? (fromJSON(json, { - plugins: [ - CustomEventPlugin, - DOMExceptionPlugin, - EventPlugin, - FormDataPlugin, - HeadersPlugin, - ReadableStreamPlugin, - RequestPlugin, - ResponsePlugin, - URLSearchParamsPlugin, - URLPlugin, - ], - }) as any) - : json - ).forEach((arg: any) => { - parsed.push(arg); - }); - } - } - if (h3Event.method === "POST") { - const contentType = request.headers.get("content-type"); + // grab bound arguments from url when no JS + if (!instance || h3Event.method === "GET") { + const args = url.searchParams.get("args"); + if (args) { + const json = JSON.parse(args); + (json.t + ? (fromJSON(json, { + plugins: [ + CustomEventPlugin, + DOMExceptionPlugin, + EventPlugin, + FormDataPlugin, + HeadersPlugin, + ReadableStreamPlugin, + RequestPlugin, + ResponsePlugin, + URLSearchParamsPlugin, + URLPlugin + ] + }) as any) + : json + ).forEach((arg: any) => { + parsed.push(arg); + }); + } + } + if (h3Event.method === "POST") { + const contentType = request.headers.get("content-type"); - if ( - contentType?.startsWith("multipart/form-data") || - contentType?.startsWith("application/x-www-form-urlencoded") - ) { - parsed.push(await event.request.formData()); - } else if (contentType?.startsWith("application/json")) { - parsed = fromJSON(await event.request.json(), { - plugins: [ - CustomEventPlugin, - DOMExceptionPlugin, - EventPlugin, - FormDataPlugin, - HeadersPlugin, - ReadableStreamPlugin, - RequestPlugin, - ResponsePlugin, - URLSearchParamsPlugin, - URLPlugin, - ], - }); - } - } - try { - let result = await provideRequestEvent(event, async () => { - /* @ts-expect-error */ - sharedConfig.context = { event }; - event.locals.serverFunctionMeta = { - id: functionId - }; - return serverFunction(...parsed); - }); + if ( + contentType?.startsWith("multipart/form-data") || + contentType?.startsWith("application/x-www-form-urlencoded") + ) { + parsed.push(await event.request.formData()); + } else if (contentType?.startsWith("application/json")) { + parsed = fromJSON(await event.request.json(), { + plugins: [ + CustomEventPlugin, + DOMExceptionPlugin, + EventPlugin, + FormDataPlugin, + HeadersPlugin, + ReadableStreamPlugin, + RequestPlugin, + ResponsePlugin, + URLSearchParamsPlugin, + URLPlugin + ] + }); + } + } + try { + let result = await provideRequestEvent(event, async () => { + /* @ts-expect-error */ + sharedConfig.context = { event }; + event.locals.serverFunctionMeta = { + id: functionId + }; + return serverFunction(...parsed); + }); - if (singleFlight && instance) { - result = await handleSingleFlight(event, result); - } + if (singleFlight && instance) { + result = await handleSingleFlight(event, result); + } - // handle responses - if (result instanceof Response) { - if (result.headers && result.headers.has("X-Content-Raw")) return result; - if (instance) { - // forward headers - if (result.headers) mergeResponseHeaders(h3Event, result.headers); - // forward non-redirect statuses - if (result.status && (result.status < 300 || result.status >= 400)) - h3Event.res.status = result.status; - if ((result as any).customBody) { - result = await (result as any).customBody(); - } else if (result.body == undefined) result = null; - } - } + // handle responses + if (result instanceof Response) { + if (result.headers && result.headers.has("X-Content-Raw")) return result; + if (instance) { + // forward headers + if (result.headers) mergeResponseHeaders(h3Event, result.headers); + // forward non-redirect statuses + if (result.status && (result.status < 300 || result.status >= 400)) + h3Event.res.status = result.status; + if ((result as any).customBody) { + result = await (result as any).customBody(); + } else if (result.body == undefined) result = null; + } + } - // handle no JS success case - if (!instance) return handleNoJS(result, request, parsed); + // handle no JS success case + if (!instance) return handleNoJS(result, request, parsed); - h3Event.res.headers.set("content-type", "text/javascript"); + h3Event.res.headers.set("content-type", "text/javascript"); - return serializeToStream(instance, result); - } catch (x) { - if (x instanceof Response) { - if (singleFlight && instance) { - x = await handleSingleFlight(event, x); - } - // forward headers - if ((x as any).headers) mergeResponseHeaders(h3Event, (x as any).headers); - // forward non-redirect statuses - if ( - (x as any).status && - (!instance || (x as any).status < 300 || (x as any).status >= 400) - ) - h3Event.res.status = (x as any).status; - if ((x as any).customBody) { - x = (x as any).customBody(); - } else if ((x as any).body === undefined) x = null; - h3Event.res.headers.set("X-Error", "true"); - } else if (instance) { - const error = - x instanceof Error ? x.message : typeof x === "string" ? x : "true"; + return serializeToStream(instance, result); + } catch (x) { + if (x instanceof Response) { + if (singleFlight && instance) { + x = await handleSingleFlight(event, x); + } + // forward headers + if ((x as any).headers) mergeResponseHeaders(h3Event, (x as any).headers); + // forward non-redirect statuses + if ((x as any).status && (!instance || (x as any).status < 300 || (x as any).status >= 400)) + h3Event.res.status = (x as any).status; + if ((x as any).customBody) { + x = (x as any).customBody(); + } else if ((x as any).body === undefined) x = null; + h3Event.res.headers.set("X-Error", "true"); + } else if (instance) { + const error = x instanceof Error ? x.message : typeof x === "string" ? x : "true"; - h3Event.res.headers.set("X-Error", error.replace(/[\r\n]+/g, "")); - } else { - x = handleNoJS(x, request, parsed, true); - } - if (instance) { - h3Event.res.headers.set("content-type", "text/javascript"); - return serializeToStream(instance, x); - } - return x; - } + h3Event.res.headers.set("X-Error", error.replace(/[\r\n]+/g, "")); + } else { + x = handleNoJS(x, request, parsed, true); + } + if (instance) { + h3Event.res.headers.set("content-type", "text/javascript"); + return serializeToStream(instance, x); + } + return x; + } } -function handleNoJS( - result: any, - request: Request, - parsed: any[], - thrown?: boolean, -) { - const url = new URL(request.url); - const isError = result instanceof Error; - let statusCode = 302; - let headers: Headers; - if (result instanceof Response) { - headers = new Headers(result.headers); - if (result.headers.has("Location")) { - headers.set( - `Location`, - new URL( - result.headers.get("Location")!, - url.origin + import.meta.env.BASE_URL, - ).toString(), - ); - statusCode = getExpectedRedirectStatus(result); - } - } else - headers = new Headers({ - Location: new URL(request.headers.get("referer")!).toString(), - }); - if (result) { - headers.append( - "Set-Cookie", - `flash=${encodeURIComponent( - JSON.stringify({ - url: url.pathname + url.search, - result: isError ? result.message : result, - thrown: thrown, - error: isError, - input: [ - ...parsed.slice(0, -1), - [...parsed[parsed.length - 1].entries()], - ], - }), - )}; Secure; HttpOnly;`, - ); - } - return new Response(null, { - status: statusCode, - headers, - }); +function handleNoJS(result: any, request: Request, parsed: any[], thrown?: boolean) { + const url = new URL(request.url); + const isError = result instanceof Error; + let statusCode = 302; + let headers: Headers; + if (result instanceof Response) { + headers = new Headers(result.headers); + if (result.headers.has("Location")) { + headers.set( + `Location`, + new URL(result.headers.get("Location")!, url.origin + import.meta.env.BASE_URL).toString() + ); + statusCode = getExpectedRedirectStatus(result); + } + } else + headers = new Headers({ + Location: new URL(request.headers.get("referer")!).toString() + }); + if (result) { + headers.append( + "Set-Cookie", + `flash=${encodeURIComponent( + JSON.stringify({ + url: url.pathname + url.search, + result: isError ? result.message : result, + thrown: thrown, + error: isError, + input: [...parsed.slice(0, -1), [...parsed[parsed.length - 1].entries()]] + }) + )}; Secure; HttpOnly;` + ); + } + return new Response(null, { + status: statusCode, + headers + }); } let App: any; function createSingleFlightHeaders(sourceEvent: FetchEvent) { - // cookie handling logic is pretty simplistic so this might be imperfect - // unclear if h3 internals are available on all platforms but we need a way to - // update request headers on the underlying H3 event. + // cookie handling logic is pretty simplistic so this might be imperfect + // unclear if h3 internals are available on all platforms but we need a way to + // update request headers on the underlying H3 event. - const headers = sourceEvent.request.headers; - const cookies = parseCookies(sourceEvent.nativeEvent); - const SetCookies = sourceEvent.response.headers.getSetCookie(); - headers.delete("cookie"); - // let useH3Internals = false; - // if (sourceEvent.nativeEvent.node?.req) { - // useH3Internals = true; - // sourceEvent.nativeEvent.node.req.headers.cookie = ""; - // } - SetCookies.forEach((cookie) => { - if (!cookie) return; - const { maxAge, expires, name, value } = parseSetCookie(cookie); - if (maxAge != null && maxAge <= 0) { - delete cookies[name]; - return; - } - if (expires != null && expires.getTime() <= Date.now()) { - delete cookies[name]; - return; - } - cookies[name] = value; - }); - Object.entries(cookies).forEach(([key, value]) => { - headers.append("cookie", `${key}=${value}`); - // useH3Internals && - // (sourceEvent.nativeEvent.node.req.headers.cookie += `${key}=${value};`); - }); + const headers = new Headers(sourceEvent.request.headers); + const cookies = parseCookies(sourceEvent.nativeEvent); + const SetCookies = sourceEvent.response.headers.getSetCookie(); + headers.delete("cookie"); + // let useH3Internals = false; + // if (sourceEvent.nativeEvent.node?.req) { + // useH3Internals = true; + // sourceEvent.nativeEvent.node.req.headers.cookie = ""; + // } + SetCookies.forEach(cookie => { + if (!cookie) return; + const { maxAge, expires, name, value } = parseSetCookie(cookie); + if (maxAge != null && maxAge <= 0) { + delete cookies[name]; + return; + } + if (expires != null && expires.getTime() <= Date.now()) { + delete cookies[name]; + return; + } + cookies[name] = value; + }); + Object.entries(cookies).forEach(([key, value]) => { + headers.append("cookie", `${key}=${value}`); + // useH3Internals && + // (sourceEvent.nativeEvent.node.req.headers.cookie += `${key}=${value};`); + }); - return headers; + return headers; } -async function handleSingleFlight( - sourceEvent: FetchEvent, - result: any, -): Promise { - let revalidate: string[]; - let url = new URL(sourceEvent.request.headers.get("referer")!).toString(); - if (result instanceof Response) { - if (result.headers.has("X-Revalidate")) - revalidate = result.headers.get("X-Revalidate")!.split(","); - if (result.headers.has("Location")) - url = new URL( - result.headers.get("Location")!, - new URL(sourceEvent.request.url).origin + - import.meta.env.BASE_URL, - ).toString(); - } - const event = { ...sourceEvent } as PageEvent; - event.request = new Request(url, { - headers: createSingleFlightHeaders(sourceEvent), - }); - return await provideRequestEvent(event, async () => { - await createPageEvent(event); - App || (App = (await import("solid-start:app")).default); - /* @ts-expect-error */ - event.router.dataOnly = revalidate || true; - /* @ts-expect-error */ - event.router.previousUrl = sourceEvent.request.headers.get("referer"); - try { - renderToString(() => { - /* @ts-expect-error */ - sharedConfig.context.event = event; - App(); - }); - } catch (e) { - console.log(e); - } +async function handleSingleFlight(sourceEvent: FetchEvent, result: any): Promise { + let revalidate: string[]; + let url = new URL(sourceEvent.request.headers.get("referer")!).toString(); + if (result instanceof Response) { + if (result.headers.has("X-Revalidate")) + revalidate = result.headers.get("X-Revalidate")!.split(","); + if (result.headers.has("Location")) + url = new URL( + result.headers.get("Location")!, + new URL(sourceEvent.request.url).origin + import.meta.env.BASE_URL + ).toString(); + } + const event = { ...sourceEvent } as PageEvent; + event.request = new Request(url, { + headers: createSingleFlightHeaders(sourceEvent) + }); + return await provideRequestEvent(event, async () => { + await createPageEvent(event); + App || (App = (await import("solid-start:app")).default); + /* @ts-expect-error */ + event.router.dataOnly = revalidate || true; + /* @ts-expect-error */ + event.router.previousUrl = sourceEvent.request.headers.get("referer"); + try { + renderToString(() => { + /* @ts-expect-error */ + sharedConfig.context.event = event; + App(); + }); + } catch (e) { + console.log(e); + } - /* @ts-expect-error */ - const body = event.router.data; - if (!body) return result; - let containsKey = false; - for (const key in body) { - if (body[key] === undefined) delete body[key]; - else containsKey = true; - } - if (!containsKey) return result; - if (!(result instanceof Response)) { - body["_$value"] = result; - result = new Response(null, { status: 200 }); - } else if ((result as any).customBody) { - body["_$value"] = (result as any).customBody(); - } - result.customBody = () => body; - result.headers.set("X-Single-Flight", "true"); - return result; - }); + /* @ts-expect-error */ + const body = event.router.data; + if (!body) return result; + let containsKey = false; + for (const key in body) { + if (body[key] === undefined) delete body[key]; + else containsKey = true; + } + if (!containsKey) return result; + if (!(result instanceof Response)) { + body["_$value"] = result; + result = new Response(null, { status: 200 }); + } else if ((result as any).customBody) { + body["_$value"] = (result as any).customBody(); + } + result.customBody = () => body; + result.headers.set("X-Single-Flight", "true"); + return result; + }); }