Problem
apps/web/app/api/og/route.ts:readBoundedText buffers the full response body (up to MAX_HTML_BYTES = 2_000_000) before returning, even though OG meta tags only live in <head>. The function's own comment states the intent — "OG parsing only needs <head>, so cap the read rather than buffering the whole body" — but the implementation does the opposite.
Impact
Per /api/og request:
- Memory: ~6-8 MB peak (2 MB
chunks: Uint8Array[] + 2 MB merged buffer + 2-4 MB UTF-16 string after TextDecoder).
- CPU: 6 regex matches over the full 2 MB string, scanning ~12 MB of bytes to extract meta tags that exist in the first 20-40 KB.
- Network: full body downloaded end-to-end even though we throw away everything after
</head>.
At link-preview scale (e.g. 20 URLs unfurled in a single chat message), peak RSS in the worker approaches 150-200 MB, and the first byte of any preview is gated on the slowest of N full-body fetches serialised through one isolate.
Suggested fix
Replace the full-body read with a streaming reader that:
- Reads chunks into a small growing buffer (cap ~64 KB).
- Scans for
</head> after each chunk (case-insensitive).
- Cancels the reader and returns the partial string as soon as the tag is found.
- Returns
null (→ 413) if the cap is hit before </head> is seen.
The existing 6 regex patterns in processHtml stay unchanged — they just run over a 30-40 KB string instead of 2 MB, which is the actual win.
Scope
Single file change. No new dependencies, no API surface change, no client-side impact. The 2 MB hard cap becomes a 64 KB <head>-only cap, but OG tags live entirely within that bound in practice (current upstream sites rarely exceed 20 KB for the head).
Problem
apps/web/app/api/og/route.ts:readBoundedTextbuffers the full response body (up toMAX_HTML_BYTES = 2_000_000) before returning, even though OG meta tags only live in<head>. The function's own comment states the intent — "OG parsing only needs<head>, so cap the read rather than buffering the whole body" — but the implementation does the opposite.Impact
Per
/api/ogrequest:chunks: Uint8Array[]+ 2 MB merged buffer + 2-4 MB UTF-16 string afterTextDecoder).</head>.At link-preview scale (e.g. 20 URLs unfurled in a single chat message), peak RSS in the worker approaches 150-200 MB, and the first byte of any preview is gated on the slowest of N full-body fetches serialised through one isolate.
Suggested fix
Replace the full-body read with a streaming reader that:
</head>after each chunk (case-insensitive).null(→ 413) if the cap is hit before</head>is seen.The existing 6 regex patterns in
processHtmlstay unchanged — they just run over a 30-40 KB string instead of 2 MB, which is the actual win.Scope
Single file change. No new dependencies, no API surface change, no client-side impact. The 2 MB hard cap becomes a 64 KB
<head>-only cap, but OG tags live entirely within that bound in practice (current upstream sites rarely exceed 20 KB for the head).