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
9 changes: 3 additions & 6 deletions packages/react/src/plugin-pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,9 @@ ${generateComponentManifest(scan, pagesDir)}
const PREAMBLE_SCRIPT = ${JSON.stringify(preambleScript)};

function serializePageData(pageObj) {
return JSON.stringify(pageObj)
.replaceAll("<", "\\u003c")
.replaceAll(">", "\\u003e")
.replaceAll("&", "\\u0026")
.replaceAll("\u2028", "\\u2028")
.replaceAll("\u2029", "\\u2029");
return JSON.stringify(pageObj).replace(/[<>&\\u2028\\u2029]/g, function(c) {
return String.fromCharCode(92) + "u" + c.charCodeAt(0).toString(16).padStart(4, "0");
});
}

export async function renderPage(pageObj, assetTags) {
Expand Down
15 changes: 12 additions & 3 deletions packages/void/src/pages/protocol.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ValidationError } from "./validation-error";
import { DEFERRED, separateDeferred, type ResolvedDeferredGroup } from "./defer";
import { resolveHead, type HeadDescriptor, type HeadConfig } from "./head";
import { isConvertedResponse } from "../runtime/response";

export { ValidationError };

Expand Down Expand Up @@ -127,9 +128,17 @@ export async function handlePageGet(
let props: Record<string, unknown> = {};
if (handler) {
const result = await handler(c);
if (result instanceof Response) {
const status = result.status;
if (status >= 300) return result;
// Responses from convertReturnValue (withValidator/middleware paths) always
// flow through to extractProps — they wrap plain return values (objects as
// 200 JSON, null/undefined as 204, strings as text/html) and must be
// unwrapped as page props.
//
// User-returned Responses: pass through errors/redirects (>= 300) and
// non-JSON 200s (file downloads, streams). JSON 200s are still extracted
// as props for backward compat with loaders using c.json().
if (result instanceof Response && !isConvertedResponse(result)) {
const ct = result.headers.get("content-type") || "";
if (result.status >= 300 || !ct.includes("application/json")) return result;
Comment thread
Brooooooklyn marked this conversation as resolved.
}
props = await extractProps(result);
}
Expand Down
29 changes: 21 additions & 8 deletions packages/void/src/runtime/response.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
/**
* Tracks responses created by convertReturnValue (as opposed to responses
* explicitly constructed by user code). Used by handlePageGet to decide
* whether a Response should be passed through or unwrapped as page props.
*/
const convertedResponses = new WeakSet<Response>();

/** Returns true if the value is a Response that was created by convertReturnValue. */
export function isConvertedResponse(value: unknown): boolean {
return value instanceof Response && convertedResponses.has(value);
}

/**
* Convert a handler's return value to a Web Response.
*
Expand All @@ -11,16 +23,17 @@ export function convertReturnValue(value: unknown): Response {
return value;
}

let response: Response;
if (value === null || value === undefined) {
return new Response(null, { status: 204 });
}

if (typeof value === "string") {
return new Response(value, {
response = new Response(null, { status: 204 });
} else if (typeof value === "string") {
response = new Response(value, {
headers: { "Content-Type": "text/html; charset=utf-8" },
});
} else {
// object, array, number, boolean → JSON
response = Response.json(value);
}

// object, array, number, boolean → JSON
return Response.json(value);
convertedResponses.add(response);
return response;
}
Loading