Skip to content
Closed
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
53 changes: 53 additions & 0 deletions packages/vue/test/integration/vue-pages-large-props.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});
Comment on lines +13 to +23

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,
});
Comment on lines +34 to +43
Comment on lines +38 to +43
} 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);
Comment on lines +25 to +52
Comment on lines +49 to +52
});
74 changes: 74 additions & 0 deletions playground/vue-pages/pages/large-props.server.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;
/** 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<string, unknown> {
const data: Record<string, unknown> = {};
// 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: <div class="test">&amp;</div>',
],
],
},
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(
`<div class="notion-block notion-text-block" data-block-id="block-${i}">` +
`<p>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.</p></div>`,
);
}
return blocks.join("\n");
}

export async function getPrerenderPaths() {
return [{}];
}

export const loader = defineHandler<Props>(() => {
return {
title: "Large Props Page",
data: generateLargeData(),
prerenderedHtml: generatePrerenderedHtml(),
};
});
13 changes: 13 additions & 0 deletions playground/vue-pages/pages/large-props.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script setup lang="ts">
import type { Props } from "./large-props.server";

const props = defineProps<Props>();
</script>

<template>
<div>
<h1>{{ props.title }}</h1>
<p>Data keys: {{ Object.keys(props.data).length }}</p>
<div v-html="props.prerenderedHtml" />
</div>
</template>
Loading