Fix local fonts in statically prerendered ImageResponse metadata route#95121
Merged
Conversation
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.
Contributor
Tests PassedCommit: 62fbfaa |
Contributor
Stats cancelledCommit: 62fbfaa |
lubieowoce
reviewed
Jun 24, 2026
lubieowoce
approved these changes
Jun 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #94957, which made cached
ImageResponseroutes 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 aBufferin the options was corrupted by that round-trip: Flight applies thetoJSONmethod that Node'sBuffercarries, so the font reached satori as a{ type: 'Buffer', data: [...] }plain object rather than binary, and satori's font parser threwTypeError: 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
Bufferis 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 duringnext 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 fontBufferthrough use-cache's own Flight serialization and reintroduce the same corruption upstream ofImageResponse, where this fix cannot reach it. A regression test loads a local font via a module-scopereadFileand asserts the route prerenders statically.