From 4cca2602106138c7fcdf4729ec91298a1da616bd Mon Sep 17 00:00:00 2001 From: Yunfei He Date: Fri, 13 Mar 2026 21:15:11 +0800 Subject: [PATCH] test: reproduce x-void-page-data header failure with large props The `x-void-page-data` response header serializes the entire page object (JSON.stringify(pageObj)) into an HTTP header on every non-deferred SSR response. For pages with large props (e.g. CMS record maps + pre-rendered HTML totaling 100KB+), this exceeds HTTP header size limits in Miniflare/workerd, causing "fetch failed" during SSG prerendering. Real-world case: a Notion-powered blog where each page returns a full ExtendedRecordMap (~30-70KB) + pre-rendered HTML (~50-200KB) as props. All blog post pages fail with "fetch failed" during `vite build` with `output: "static"`. This commit adds: - A playground page (large-props) that generates ~300KB of props simulating CMS data with Unicode content - A failing integration test that builds the vue-pages playground in SSG mode and verifies prerendering succeeds Co-Authored-By: Claude Opus 4.6 --- .../integration/vue-pages-large-props.test.ts | 53 +++++++++++++ .../vue-pages/pages/large-props.server.ts | 74 +++++++++++++++++++ playground/vue-pages/pages/large-props.vue | 13 ++++ 3 files changed, 140 insertions(+) create mode 100644 packages/vue/test/integration/vue-pages-large-props.test.ts create mode 100644 playground/vue-pages/pages/large-props.server.ts create mode 100644 playground/vue-pages/pages/large-props.vue 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 @@ + + +