From c752d848d6007f0b48b23b392bb5a45476e661e2 Mon Sep 17 00:00:00 2001
From: David Pine <7679720+IEvangelist@users.noreply.github.com>
Date: Mon, 19 Oct 2026 22:29:31 -0500
Subject: [PATCH 1/4] Fix OG Viewer failing on pages over the 6 MiB fetch cap
fetchUrl rejected any response larger than its 6 MiB safety cap, so
https://aspire.dev/reference/api/csharp/ (6.4 MB of HTML) never loaded in
the canvas, even though its title and og:image sit in the first ~44 KB.
The agent-readiness probes had the same problem with their 256 KiB cap and
silently reported oversized responses as missing.
Add an opt-in onLimit: "truncate" mode to fetchUrl that keeps exactly the
first maxBytes bytes, aborts the connection, and resolves with
truncated: true. The default still rejects. Use it where a partial
document is useful: metadata loading, the Browse tab's main document
(non-HTML bodies are still rejected because a cut-off file is corrupt),
and the agent-readiness probes (a cut-off size is shown as "256 KB+").
Subresources, /api/img and /api/raw keep their current behavior.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 65ef8eed-2fb4-4090-baf5-0c8664d156e6
---
.github/extensions/og-preview/README.md | 5 +-
.github/extensions/og-preview/extension.mjs | 7 +-
.../og-preview/lib/agent-readiness.mjs | 14 ++-
.../extensions/og-preview/lib/http-fetch.mjs | 41 +++++---
.../og-preview/tests/agent-readiness.test.mjs | 53 ++++++++++
.../og-preview/tests/http-fetch.test.mjs | 99 ++++++++++++++++++-
.../og-preview/tests/server.test.mjs | 39 ++++++++
7 files changed, 236 insertions(+), 22 deletions(-)
create mode 100644 .github/extensions/og-preview/tests/agent-readiness.test.mjs
diff --git a/.github/extensions/og-preview/README.md b/.github/extensions/og-preview/README.md
index 9cfc22913b..9c2379c48e 100644
--- a/.github/extensions/og-preview/README.md
+++ b/.github/extensions/og-preview/README.md
@@ -38,7 +38,10 @@ port) that serves the static UI from `ui/` and a JSON API:
| `GET /events` | Server-Sent Events; agent-driven loads are pushed here |
The target page is fetched and parsed server-side (no external dependencies),
-which sidesteps browser CORS and lets it reach `localhost`.
+which sidesteps browser CORS and lets it reach `localhost`. Page and resource
+fetches are capped at 6 MiB: an oversized HTML page is truncated rather than
+rejected (its metadata lives in the head), while oversized scripts, styles,
+images, and other files fail to load.
## Network access and isolation
diff --git a/.github/extensions/og-preview/extension.mjs b/.github/extensions/og-preview/extension.mjs
index 077b10d316..8f5d302261 100644
--- a/.github/extensions/og-preview/extension.mjs
+++ b/.github/extensions/og-preview/extension.mjs
@@ -77,7 +77,8 @@ async function serveAsset(res, name) {
/** Fetch a target URL and parse its OpenGraph metadata. */
async function loadMetadata(rawUrl, policy) {
const target = normalizeUrl(rawUrl);
- const result = await fetchUrl(target, policy);
+ // Metadata lives in the document head, so an oversized page is cut, not rejected.
+ const result = await fetchUrl(target, { ...policy, onLimit: "truncate" });
if (result.status >= 400) {
throw new Error(`Target responded with HTTP ${result.status}.`);
}
@@ -88,6 +89,7 @@ async function loadMetadata(rawUrl, policy) {
const data = parseMetadata(html, result.url);
data.requestedUrl = result.url;
data.httpStatus = result.status;
+ data.truncated = result.truncated;
return data;
}
@@ -666,6 +668,7 @@ async function handleRequest(entry, req, res) {
...policy,
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
timeoutMs: 15000,
+ onLimit: "truncate",
});
const ct = r.contentType || "";
const isHtml = !ct || /text\/html|application\/xhtml\+xml|\/xml|text\/plain/i.test(ct);
@@ -673,6 +676,8 @@ async function handleRequest(entry, req, res) {
res.setHeader("Access-Control-Allow-Origin", "*");
const appOrigin = proxyOrigin;
if (!isHtml) {
+ // A cut-off image or PDF would be corrupt; only documents may be partial.
+ if (r.truncated) throw new Error("Response exceeded the size limit.");
// Serve non-HTML targets (images, PDFs, …) verbatim so links to
// them still render inside the browse frame.
res.statusCode = r.status >= 400 ? r.status : 200;
diff --git a/.github/extensions/og-preview/lib/agent-readiness.mjs b/.github/extensions/og-preview/lib/agent-readiness.mjs
index 9f9f159ba6..604f0d7f20 100644
--- a/.github/extensions/og-preview/lib/agent-readiness.mjs
+++ b/.github/extensions/og-preview/lib/agent-readiness.mjs
@@ -33,6 +33,9 @@ async function probe(url, opts = {}) {
timeoutMs: 7000,
maxRedirects: 4,
maxBytes: 256 * 1024,
+ // Presence, headers, and the leading bytes are what the checks read, so
+ // an oversized response is cut short rather than reported as missing.
+ onLimit: "truncate",
...opts,
});
return {
@@ -42,6 +45,7 @@ async function probe(url, opts = {}) {
contentType: r.contentType || "",
body: r.body,
bytes: r.body ? r.body.length : 0,
+ truncated: Boolean(r.truncated),
finalUrl: r.url,
};
} catch {
@@ -67,10 +71,10 @@ function isJsonish(r) {
return head.startsWith("{") || head.startsWith("[");
}
-function fmtBytes(n) {
+function fmtBytes(n, truncated = false) {
if (!n) return "0 B";
- if (n < 1024) return `${n} B`;
- return `${(n / 1024).toFixed(n < 10240 ? 1 : 0)} KB`;
+ const size = n < 1024 ? `${n} B` : `${(n / 1024).toFixed(n < 10240 ? 1 : 0)} KB`;
+ return truncated ? `${size}+` : size;
}
function missDetail(r) {
@@ -172,7 +176,7 @@ export async function checkAgentReadiness(rawUrl, policy = {}) {
label: "robots.txt",
status: scored(hasRobots, true),
detail: hasRobots
- ? `Found · ${fmtBytes(robots.bytes)}${sitemapInRobots ? " · declares Sitemap" : ""}`
+ ? `Found · ${fmtBytes(robots.bytes, robots.truncated)}${sitemapInRobots ? " · declares Sitemap" : ""}`
: missDetail(robots),
},
{
@@ -202,7 +206,7 @@ export async function checkAgentReadiness(rawUrl, policy = {}) {
label: "llms.txt",
status: scored(hasLlms, true),
detail: hasLlms
- ? `Found · ${fmtBytes(llms.bytes)}${hasLlmsFull ? " · llms-full.txt too" : ""}`
+ ? `Found · ${fmtBytes(llms.bytes, llms.truncated)}${hasLlmsFull ? " · llms-full.txt too" : ""}`
: missDetail(llms),
},
{
diff --git a/.github/extensions/og-preview/lib/http-fetch.mjs b/.github/extensions/og-preview/lib/http-fetch.mjs
index 5370349eb9..92bf258155 100644
--- a/.github/extensions/og-preview/lib/http-fetch.mjs
+++ b/.github/extensions/og-preview/lib/http-fetch.mjs
@@ -109,7 +109,12 @@ const MAX_BYTES = 6 * 1024 * 1024; // 6 MB safety cap
/**
* Fetch a URL, following redirects. Resolves with
- * { url, status, headers, contentType, body: Buffer }.
+ * { url, status, headers, contentType, body: Buffer, truncated: boolean }.
+ *
+ * A body larger than `maxBytes` rejects by default. With `onLimit: "truncate"`
+ * it resolves with exactly the first `maxBytes` bytes and `truncated: true`
+ * instead. Only opt in for content that stays useful when cut short, such as an
+ * HTML document; a truncated image, script, or archive would be corrupt.
*/
export function fetchUrl(rawUrl, options = {}) {
const {
@@ -117,6 +122,7 @@ export function fetchUrl(rawUrl, options = {}) {
timeoutMs = 15000,
accept = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
maxBytes = MAX_BYTES,
+ onLimit = "error",
allowPrivateNetwork = ALLOW_PRIVATE_NETWORK,
authorizedOrigin = null,
} = options;
@@ -193,27 +199,34 @@ export function fetchUrl(rawUrl, options = {}) {
const chunks = [];
let total = 0;
let aborted = false;
+ const finish = (truncated) =>
+ resolve({
+ url: parsed.toString(),
+ status,
+ headers: res.headers,
+ contentType: String(res.headers["content-type"] || ""),
+ body: Buffer.concat(chunks),
+ truncated,
+ });
res.on("data", (chunk) => {
- total += chunk.length;
- if (total > maxBytes) {
+ if (aborted) return;
+ if (total + chunk.length > maxBytes) {
aborted = true;
req.destroy();
res.destroy();
- return reject(
- new Error(`Response exceeded the ${maxBytes}-byte limit.`),
- );
+ if (onLimit !== "truncate") {
+ return reject(
+ new Error(`Response exceeded the ${maxBytes}-byte limit.`),
+ );
+ }
+ chunks.push(chunk.subarray(0, maxBytes - total));
+ return finish(true);
}
+ total += chunk.length;
chunks.push(chunk);
});
res.on("end", () => {
- if (aborted) return;
- resolve({
- url: parsed.toString(),
- status,
- headers: res.headers,
- contentType: String(res.headers["content-type"] || ""),
- body: Buffer.concat(chunks),
- });
+ if (!aborted) finish(false);
});
res.on("error", (err) => {
if (!aborted) reject(err);
diff --git a/.github/extensions/og-preview/tests/agent-readiness.test.mjs b/.github/extensions/og-preview/tests/agent-readiness.test.mjs
new file mode 100644
index 0000000000..f767c59050
--- /dev/null
+++ b/.github/extensions/og-preview/tests/agent-readiness.test.mjs
@@ -0,0 +1,53 @@
+import assert from "node:assert/strict";
+import { test } from "node:test";
+import { once } from "node:events";
+import { createServer } from "node:http";
+import dns from "node:dns/promises";
+import { authorizePreview } from "../lib/http-fetch.mjs";
+import { checkAgentReadiness } from "../lib/agent-readiness.mjs";
+
+// Probes read at most 256 KiB, so these responses are all larger than that.
+const oversized = 300 * 1024;
+
+test("probe responses over the size cap are still detected, not reported missing", async (t) => {
+ t.mock.method(dns, "resolveTxt", async () => {
+ throw new Error("ENOTFOUND");
+ });
+ const robots = "User-agent: GPTBot\nDisallow: /\nSitemap: /sitemap.xml\n".padEnd(oversized, "#");
+ const server = createServer((req, res) => {
+ const send = (type, body, headers = {}) => {
+ res.writeHead(200, { "Content-Type": type, ...headers });
+ res.end(body);
+ };
+ if (req.url === "/robots.txt") return send("text/plain", robots);
+ if (req.url === "/llms.txt") return send("text/markdown", "# Example\n> Docs for agents.\n");
+ if (req.url === "/llms-full.txt") return send("text/markdown", "# Example\n".padEnd(oversized, "x"));
+ if (req.url === "/") {
+ if (/markdown/i.test(req.headers.accept || "")) return send("text/markdown", "# Home\n".padEnd(oversized, "x"));
+ return send("text/html", "
Home".padEnd(oversized, " "), {
+ Link: '; rel="api-catalog"',
+ });
+ }
+ res.writeHead(404, { "Content-Type": "text/plain" });
+ res.end("Not found");
+ });
+ server.listen(0, "127.0.0.1");
+ await once(server, "listening");
+ const url = `http://127.0.0.1:${server.address().port}/`;
+ try {
+ const report = await checkAgentReadiness(url, await authorizePreview(url, false));
+ const checks = Object.fromEntries(report.categories.flatMap((c) => c.checks).map((c) => [c.id, c]));
+
+ assert.equal(checks.robots.status, "pass");
+ assert.match(checks.robots.detail, /Found · 256 KB\+ · declares Sitemap/, "a cut-off size is not shown as the full size");
+ assert.equal(checks["ai-bots"].status, "pass");
+ assert.equal(checks.llms.status, "pass");
+ assert.match(checks.llms.detail, /llms-full\.txt too/);
+ assert.equal(checks.markdown.status, "pass");
+ assert.equal(checks["link-headers"].status, "pass");
+ assert.match(checks["link-headers"].detail, /api-catalog/);
+ } finally {
+ server.closeAllConnections();
+ await new Promise((resolve) => server.close(resolve));
+ }
+});
diff --git a/.github/extensions/og-preview/tests/http-fetch.test.mjs b/.github/extensions/og-preview/tests/http-fetch.test.mjs
index 3bb360b27b..d2c12f3cf1 100644
--- a/.github/extensions/og-preview/tests/http-fetch.test.mjs
+++ b/.github/extensions/og-preview/tests/http-fetch.test.mjs
@@ -1,9 +1,10 @@
import assert from "node:assert/strict";
import { test } from "node:test";
-import { EventEmitter } from "node:events";
+import { EventEmitter, once } from "node:events";
import http from "node:http";
import https from "node:https";
import dns from "node:dns/promises";
+import { setTimeout as delay } from "node:timers/promises";
import { authorizePreview, fetchUrl, normalizeUrl } from "../lib/http-fetch.mjs";
function transport(t, library = http, responses = [{}]) {
@@ -153,3 +154,99 @@ test("a public resource cannot redirect back to an otherwise authorized local or
await assert.rejects(fetchUrl("http://8.8.8.8/", await authorizePreview("http://127.0.0.1/")), /authorized/);
assert.equal(requests.length, 1);
});
+
+// The size-cap tests use a real loopback server: the mocked transport above has
+// no `destroy`, which is exactly what a capped response needs.
+async function withServer(handler, run) {
+ const server = http.createServer(handler);
+ server.listen(0, "127.0.0.1");
+ await once(server, "listening");
+ const url = `http://127.0.0.1:${server.address().port}/`;
+ try {
+ await run(url, await authorizePreview(url, false));
+ } finally {
+ server.closeAllConnections();
+ await new Promise((resolve) => server.close(resolve));
+ }
+}
+
+function within(promise, message, ms = 5000) {
+ let timer;
+ const timeout = new Promise((_, reject) => {
+ timer = setTimeout(() => reject(new Error(message)), ms);
+ });
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
+}
+
+const page = Buffer.from("0123456789".repeat(300));
+const deliveries = {
+ "one chunk": (res) => res.end(page),
+ "several chunks": async (res) => {
+ for (let offset = 0; offset < page.length && !res.destroyed; offset += 600) {
+ res.write(page.subarray(offset, offset + 600));
+ await delay(20);
+ }
+ res.end();
+ },
+};
+
+for (const [shape, send] of Object.entries(deliveries)) {
+ const serve = (req, res) => {
+ res.writeHead(200, { "Content-Type": "text/html" });
+ send(res);
+ };
+
+ test(`a body over the byte cap is rejected by default (${shape})`, async () => {
+ await withServer(serve, async (url, policy) => {
+ await assert.rejects(fetchUrl(url, { ...policy, maxBytes: 1000 }), /exceeded the 1000-byte limit/);
+ await assert.rejects(
+ fetchUrl(url, { ...policy, maxBytes: 1000, onLimit: "error" }),
+ /exceeded the 1000-byte limit/,
+ );
+ });
+ });
+
+ test(`truncate mode keeps exactly the first maxBytes bytes (${shape})`, async () => {
+ await withServer(serve, async (url, policy) => {
+ const result = await fetchUrl(url, { ...policy, maxBytes: 1000, onLimit: "truncate" });
+ assert.equal(result.status, 200);
+ assert.equal(result.contentType, "text/html");
+ assert.equal(result.truncated, true);
+ assert.equal(result.body.length, 1000);
+ assert.ok(result.body.equals(page.subarray(0, 1000)));
+ });
+ });
+}
+
+for (const onLimit of ["error", "truncate"]) {
+ test(`a body at or under the cap is returned whole in ${onLimit} mode`, async () => {
+ await withServer((req, res) => res.end(page), async (url, policy) => {
+ for (const maxBytes of [page.length, page.length + 1]) {
+ const result = await fetchUrl(url, { ...policy, maxBytes, onLimit });
+ assert.equal(result.truncated, false);
+ assert.ok(result.body.equals(page));
+ }
+ });
+ });
+}
+
+test("truncate mode closes the connection instead of draining the rest of the body", async () => {
+ const closed = Promise.withResolvers();
+ const endless = (req, res) => {
+ res.writeHead(200, { "Content-Type": "text/html" });
+ const timer = setInterval(() => res.write(Buffer.alloc(1024, "a")), 1);
+ res.on("close", () => {
+ clearInterval(timer);
+ closed.resolve();
+ });
+ };
+ await withServer(endless, async (url, policy) => {
+ const result = await within(
+ fetchUrl(url, { ...policy, maxBytes: 4096, onLimit: "truncate" }),
+ "The capped response never resolved.",
+ );
+ assert.equal(result.truncated, true);
+ assert.equal(result.body.length, 4096);
+ await within(closed.promise, "The upstream connection stayed open.");
+ });
+});
diff --git a/.github/extensions/og-preview/tests/server.test.mjs b/.github/extensions/og-preview/tests/server.test.mjs
index 601a391759..56aefb94c9 100644
--- a/.github/extensions/og-preview/tests/server.test.mjs
+++ b/.github/extensions/og-preview/tests/server.test.mjs
@@ -200,6 +200,45 @@ test("rejects unrelated loopback origins for all fetch surfaces", async () => {
assert.equal((await fetch(resource)).status, 502);
});
+test("an oversized HTML page still loads metadata and the browse document", async () => {
+ const oversized = 7 * 1024 * 1024;
+ const document = Buffer.concat([
+ Buffer.from(
+ 'Huge page' +
+ '',
+ ),
+ Buffer.alloc(oversized, "
filler
"),
+ ]);
+ const hugeTarget = createServer((req, res) => {
+ const isDocument = req.url === "/";
+ res.setHeader("Content-Type", isDocument ? "text/html" : req.url === "/big.pdf" ? "application/pdf" : "text/javascript");
+ res.end(isDocument ? document : Buffer.alloc(oversized, "x"));
+ });
+ hugeTarget.listen(0, "127.0.0.1");
+ await once(hugeTarget, "listening");
+ const origin = `http://127.0.0.1:${hugeTarget.address().port}`;
+ try {
+ const selection = await fetch(api("/api/fetch?select=1&silent=1&u=" + encodeURIComponent(origin)));
+ const metadata = await selection.json();
+ assert.equal(metadata.resolved.title, "Huge page");
+ assert.equal(metadata.resolved.image, `${origin}/huge.png`);
+ assert.equal(metadata.truncated, true);
+
+ const html = await (await fetch(api("/api/proxy?u=" + encodeURIComponent(origin)))).text();
+ assert.match(html, /Huge page<\/title>/);
+ assert.doesNotMatch(html, /Couldn't load this page/);
+
+ const script = new URL(resourceUrl(html).href.replace("/external.js", "/big.js"));
+ assert.equal((await fetch(script)).status, 502, "oversized subresources are rejected, not truncated");
+ const pdf = await fetch(api("/api/proxy?u=" + encodeURIComponent(`${origin}/big.pdf`)));
+ assert.match(await pdf.text(), /Couldn't load this page/, "an oversized non-HTML document is not served cut off");
+ } finally {
+ await fetch(api("/api/fetch?select=1&silent=1&u=" + encodeURIComponent(targetUrl)));
+ hugeTarget.closeAllConnections();
+ await new Promise((resolve) => hugeTarget.close(resolve));
+ }
+});
+
test("standalone get_metadata action explicitly authorizes localhost", async () => {
const action = canvas.actions.find((action) => action.name === "get_metadata");
const result = await action.handler({ input: { url: targetUrl } });
From b1d2a82d5908a1e6ba974bdd50c59f55fd20fe0f Mon Sep 17 00:00:00 2001
From: David Pine <7679720+IEvangelist@users.noreply.github.com>
Date: Mon, 19 Oct 2026 22:33:35 -0500
Subject: [PATCH 2/4] Avoid doubled error prefix in the OG Viewer banner
The server's /api/fetch and /api/agent-readiness errors already begin
with "Couldn't ...", and the UI prepended its own "Couldn't load
metadata:" / "Couldn't run agent-readiness checks:" context, so the
banner read "Couldn't load metadata: Couldn't load metadata. Check the
URL and selected-origin access."
Add a small failureMessage() helper that only prepends the context when
the message doesn't already start with "Couldn't". Server strings are
unchanged (tests assert on them and they are the fixed, no-leak
messages); other errors, such as network failures, still get the prefix.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 65ef8eed-2fb4-4090-baf5-0c8664d156e6
---
.github/extensions/og-preview/ui/app.js | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/.github/extensions/og-preview/ui/app.js b/.github/extensions/og-preview/ui/app.js
index 48b603d61b..10d9875d31 100644
--- a/.github/extensions/og-preview/ui/app.js
+++ b/.github/extensions/og-preview/ui/app.js
@@ -2015,7 +2015,7 @@ async function refreshAgentReadiness(data) {
if (seq !== arSeq) return;
body.replaceChildren(
el("div", { class: "ar-error" }, [
- el("span", { text: `Couldn't run agent-readiness checks: ${err.message}` }),
+ el("span", { text: failureMessage("Couldn't run agent-readiness checks", err) }),
]),
);
}
@@ -2101,6 +2101,12 @@ function setFooterOpen(open) {
/* ---------------- Status + load ---------------- */
+/* Server errors are already complete sentences ("Couldn't load metadata. …"),
+ so only add context to messages that lack it, such as network failures. */
+function failureMessage(prefix, err) {
+ return /^Couldn't/.test(err.message) ? err.message : `${prefix}: ${err.message}`;
+}
+
function setStatus(kind, message) {
if (!kind) {
statusEl.hidden = true;
@@ -2196,7 +2202,7 @@ async function load(rawUrl, opts) {
});
setStatus(null);
} catch (err) {
- setStatus("error", `Couldn't load metadata: ${err.message}`);
+ setStatus("error", failureMessage("Couldn't load metadata", err));
$("#footer-summary").textContent = `Failed to load ${url}`;
} finally {
finishProgress();
From aa9e0558653fa8e14296129b990fd98159bb03d0 Mon Sep 17 00:00:00 2001
From: David Pine <7679720+IEvangelist@users.noreply.github.com>
Date: Mon, 19 Oct 2026 23:32:51 -0500
Subject: [PATCH 3/4] Let the OG Viewer Browse tab load documents up to 16 MiB
The Browse tab runs the live document, but the proxy cut it at the 6 MiB fetch cap. The C# API reference page is 6.4 MB and inlines a 3.7 MB search index ahead of its interactive content, so Browse rendered everything up to the cut and dropped the search UI, footer, and closing tags.
Give the main browse document a 16 MiB cap, still truncating rather than rejecting. Metadata reads, sub-resources, and non-HTML files keep their existing limits.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ed12c02d-40f7-44b1-b708-2f2517aa89e7
---
.github/extensions/og-preview/README.md | 4 ++-
.github/extensions/og-preview/extension.mjs | 6 ++++
.../og-preview/tests/server.test.mjs | 31 ++++++++++++-------
3 files changed, 29 insertions(+), 12 deletions(-)
diff --git a/.github/extensions/og-preview/README.md b/.github/extensions/og-preview/README.md
index 9c2379c48e..31777cda72 100644
--- a/.github/extensions/og-preview/README.md
+++ b/.github/extensions/og-preview/README.md
@@ -41,7 +41,9 @@ The target page is fetched and parsed server-side (no external dependencies),
which sidesteps browser CORS and lets it reach `localhost`. Page and resource
fetches are capped at 6 MiB: an oversized HTML page is truncated rather than
rejected (its metadata lives in the head), while oversized scripts, styles,
-images, and other files fail to load.
+images, and other files fail to load. The Browse tab runs the whole document,
+and large API reference pages put their interactive content after several MB of
+inline data, so the page it loads gets a 16 MiB cap; past that it is cut off.
## Network access and isolation
diff --git a/.github/extensions/og-preview/extension.mjs b/.github/extensions/og-preview/extension.mjs
index 8f5d302261..eeb527e0bb 100644
--- a/.github/extensions/og-preview/extension.mjs
+++ b/.github/extensions/og-preview/extension.mjs
@@ -41,6 +41,11 @@ function githubBlobToRaw(value) {
return value;
}
+// The browse frame runs the whole document, so it gets more room than a metadata
+// read: API reference pages inline several MB of search data ahead of their
+// interactive content, which a 6 MiB cut would drop.
+const BROWSE_DOC_MAX_BYTES = 16 * 1024 * 1024;
+
const CONTENT_TYPES = {
".html": "text/html; charset=utf-8",
".css": "text/css; charset=utf-8",
@@ -668,6 +673,7 @@ async function handleRequest(entry, req, res) {
...policy,
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
timeoutMs: 15000,
+ maxBytes: BROWSE_DOC_MAX_BYTES,
onLimit: "truncate",
});
const ct = r.contentType || "";
diff --git a/.github/extensions/og-preview/tests/server.test.mjs b/.github/extensions/og-preview/tests/server.test.mjs
index 56aefb94c9..4d24697a91 100644
--- a/.github/extensions/og-preview/tests/server.test.mjs
+++ b/.github/extensions/og-preview/tests/server.test.mjs
@@ -201,18 +201,21 @@ test("rejects unrelated loopback origins for all fetch surfaces", async () => {
});
test("an oversized HTML page still loads metadata and the browse document", async () => {
- const oversized = 7 * 1024 * 1024;
- const document = Buffer.concat([
- Buffer.from(
- 'Huge page' +
- '',
- ),
- Buffer.alloc(oversized, "
filler
"),
- ]);
+ const oversized = 7 * 1024 * 1024; // over the 6 MiB metadata and resource cap
+ const beyondBrowse = 17 * 1024 * 1024; // over the 16 MiB browse document cap
+ const head =
+ 'Huge page' +
+ '';
+ const page = (size) =>
+ Buffer.concat([Buffer.from(head), Buffer.alloc(size, "