diff --git a/packages/vue/test/integration/vue-pages-large-props.test.ts b/packages/vue/test/integration/vue-pages-large-props.test.ts new file mode 100644 index 000000000..8de4ef5cd --- /dev/null +++ b/packages/vue/test/integration/vue-pages-large-props.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it, beforeAll, afterAll } from "vitest"; +import { execSync } from "node:child_process"; +import { join } from "node:path"; +import { existsSync, writeFileSync, unlinkSync } from "node:fs"; + +const ROOT = join(import.meta.dirname, "..", "..", "..", ".."); +const PLAYGROUND = join(ROOT, "playground/vue-pages"); +const VOID_JSON = join(PLAYGROUND, "void.json"); + +describe("x-void-page-data header with large props", () => { + let voidJsonExisted: boolean; + + beforeAll(() => { + // Check if void.json already exists (shouldn't, but be safe) + voidJsonExisted = existsSync(VOID_JSON); + }); + + afterAll(() => { + // Clean up: remove void.json if we created it + if (!voidJsonExisted && existsSync(VOID_JSON)) { + unlinkSync(VOID_JSON); + } + }); + + it("SSG prerender should not fail for pages with large props (~300KB)", () => { + // Enable SSG mode so the build prerenders pages via Miniflare. + // The x-void-page-data header serializes the full page object (~300KB+) + // into an HTTP response header, which exceeds Miniflare's header size + // limits and causes "fetch failed" during prerendering. + // + // Real-world case: Notion-powered blog (musubi) where each page returns + // a full ExtendedRecordMap (~30-70KB) + pre-rendered HTML (~50-200KB). + // All 16 blog post pages fail with "fetch failed" during `vite build`. + writeFileSync(VOID_JSON, JSON.stringify({ output: "static" }, null, 2)); + + let buildOutput: string; + try { + buildOutput = execSync("pnpm --dir playground/vue-pages exec vite build", { + cwd: ROOT, + stdio: "pipe", + encoding: "utf-8", + timeout: 60_000, + }); + } catch (e: any) { + // Build may "succeed" with warnings for failed prerenders, or may throw + buildOutput = (e.stdout ?? "") + (e.stderr ?? ""); + } + + // The large-props page should prerender successfully without "fetch failed" + expect(buildOutput).not.toContain("large-props failed"); + expect(buildOutput).toContain("large-props.html"); + }, 60_000); +}); diff --git a/playground/vue-pages/pages/large-props.server.ts b/playground/vue-pages/pages/large-props.server.ts new file mode 100644 index 000000000..1d1729f30 --- /dev/null +++ b/playground/vue-pages/pages/large-props.server.ts @@ -0,0 +1,74 @@ +import { defineHandler } from "void"; + +export interface Props { + title: string; + /** Simulates a large payload (e.g. CMS record map, pre-rendered HTML) */ + data: Record; + /** Simulates pre-rendered HTML from a CMS */ + prerenderedHtml: string; +} + +/** + * Reproduces: x-void-page-data header serializes the entire page object into + * an HTTP response header. For pages with large props (e.g. CMS record maps, + * pre-rendered HTML), this exceeds HTTP header size limits, causing fetch errors. + * + * Real-world example: a Notion-powered blog where each page returns a full + * ExtendedRecordMap (~30-70KB) plus pre-rendered HTML (~50-200KB). + * + * The issue manifests in SSG prerendering via Miniflare's dispatchFetch(), + * where large/non-ASCII header values cause "fetch failed" errors. + */ +function generateLargeData(): Record { + const data: Record = {}; + // Simulate a CMS record map with many blocks (~200KB) + // Include Unicode content (common in CMS data) which can trigger + // stricter header validation in HTTP transports + for (let i = 0; i < 500; i++) { + data[`block_${i}`] = { + id: `block-${i}-${"abcdef0123456789".repeat(12)}`, + type: "paragraph", + properties: { + title: [ + [ + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " + + "Sed do eiusmod tempor incididunt ut labore — et dolore magna aliqua. " + + "日本語テスト・中文测试・한국어 테스트. " + + 'Emoji: 🚀📦✨ Special chars:
&
', + ], + ], + }, + content: [`child-${i}-a`, `child-${i}-b`, `child-${i}-c`], + format: { block_width: 680, block_height: 400, block_color: "gray" }, + created_time: 1700000000000, + last_edited_time: 1700000000000, + }; + } + return data; +} + +function generatePrerenderedHtml(): string { + // Simulate pre-rendered HTML from a CMS (~100KB) + const blocks: string[] = []; + for (let i = 0; i < 300; i++) { + blocks.push( + `
` + + `

Lorem ipsum dolor sit amet, consectetur adipiscing elit. ` + + `Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ` + + `Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.

`, + ); + } + return blocks.join("\n"); +} + +export async function getPrerenderPaths() { + return [{}]; +} + +export const loader = defineHandler(() => { + return { + title: "Large Props Page", + data: generateLargeData(), + prerenderedHtml: generatePrerenderedHtml(), + }; +}); diff --git a/playground/vue-pages/pages/large-props.vue b/playground/vue-pages/pages/large-props.vue new file mode 100644 index 000000000..6117c6393 --- /dev/null +++ b/playground/vue-pages/pages/large-props.vue @@ -0,0 +1,13 @@ + + +