Skip to content

Fix local fonts in statically prerendered ImageResponse metadata route#95121

Merged
unstubbable merged 2 commits into
canaryfrom
hl/fix-og-custom-font-issue
Jun 24, 2026
Merged

Fix local fonts in statically prerendered ImageResponse metadata route#95121
unstubbable merged 2 commits into
canaryfrom
hl/fix-og-custom-font-issue

Conversation

@unstubbable

Copy link
Copy Markdown
Contributor

Follow-up to #94957, which made cached ImageResponse routes statically prerenderable by serializing the constructor arguments through React Flight (to build a cache key and to run the element's async Server Components once). A custom font passed as a Buffer in the options was corrupted by that round-trip: Flight applies the toJSON method that Node's Buffer carries, so the font reached satori as a { type: 'Buffer', data: [...] } plain object rather than binary, and satori's font parser threw TypeError: First argument to DataView constructor must be an ArrayBuffer. The crash happened even without a user 'use cache' directive, because the serialization runs unconditionally during the prerender.

We now serialize only the element through Flight, which is the only part that needs async resolution and dynamic-input detection, and pair the resolved element with the original in-memory options when handing the tree to satori. The font Buffer is therefore never serialized and reaches satori intact. The cache key no longer comes from the serialized arguments either; it is derived from a SHA-256 hash of the serialized element combined with a content hash of the options, with binary font data hashed by its bytes and everything else by a sorted, type-tagged walk. Besides fixing the crash, this keeps font bytes out of the cache key and removes the "Uint8Array objects are not supported" warning that the dev React runtime emitted during next build --debug-prerender.

The custom-fonts documentation example now loads the font once at module scope. This keeps the route statically generated, since reading inside the component would be uncached I/O that Cache Components treats as dynamic, and it steers users away from wrapping the read in 'use cache', which would route the font Buffer through use-cache's own Flight serialization and reintroduce the same corruption upstream of ImageResponse, where this fix cannot reach it. A regression test loads a local font via a module-scope readFile and asserts the route prerenders statically.

Follow-up to #94957, which made cached `ImageResponse` routes statically
prerenderable by serializing the constructor arguments through React
Flight (to build a cache key and to run the element's async Server
Components once). A custom font passed as a `Buffer` in the options was
corrupted by that round-trip: Flight applies the `toJSON` method that
Node's `Buffer` carries, so the font reached satori as a `{ type:
'Buffer', data: [...] }` plain object rather than binary, and satori's
font parser threw `TypeError: First argument to DataView constructor
must be an ArrayBuffer`. The crash happened even without a user `'use
cache'` directive, because the serialization runs unconditionally during
the prerender.

We now serialize only the element through Flight, which is the only part
that needs async resolution and dynamic-input detection, and pair the
resolved element with the original in-memory options when handing the
tree to satori. The font `Buffer` is therefore never serialized and
reaches satori intact. The cache key no longer comes from the serialized
arguments either; it is derived from a SHA-256 hash of the serialized
element combined with a content hash of the options, with binary font
data hashed by its bytes and everything else by a sorted, type-tagged
walk. Besides fixing the crash, this keeps font bytes out of the cache
key and removes the "Uint8Array objects are not supported" warning that
the dev React runtime emitted during `next build --debug-prerender`.

The custom-fonts documentation example now loads the font once at module
scope. This keeps the route statically generated, since reading inside
the component would be uncached I/O that Cache Components treats as
dynamic, and it steers users away from wrapping the read in `'use
cache'`, which would route the font `Buffer` through use-cache's own
Flight serialization and reintroduce the same corruption upstream of
`ImageResponse`, where this fix cannot reach it. A regression test loads
a local font via a module-scope `readFile` and asserts the route
prerenders statically.
@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Tests Passed

Commit: 62fbfaa

@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Stats cancelled

Commit: 62fbfaa
View workflow run

@unstubbable unstubbable marked this pull request as ready for review June 24, 2026 12:07
@unstubbable unstubbable requested a review from lubieowoce June 24, 2026 17:51
Comment thread packages/next/src/server/og/cache-image-response.ts
@unstubbable unstubbable enabled auto-merge (squash) June 24, 2026 20:17
@unstubbable unstubbable merged commit e194a06 into canary Jun 24, 2026
128 of 129 checks passed
@unstubbable unstubbable deleted the hl/fix-og-custom-font-issue branch June 24, 2026 20:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants