cloudflare / cloudflare/workerd

IMAGES.input(ArrayBuffer)` is treated as `text()` and throws `Cannot read properties of undefined (reading 'font')

Open
#7,097 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
C++
Stars
8.7k
Forks
739
Avg merge
2d 20h
Merged PRs (30d)
174

Description

## Summary

`env.IMAGES.input(arrayBuffer).transform(...).output(...)` throws in production:

```
TypeError: Cannot read properties of undefined (reading 'font')
```

JPEG/PNG from `fetch().arrayBuffer()`, `R2Object.arrayBuffer()`, and `File.arrayBuffer()` all fail. `new Blob([buffer]).stream()` works.

This is not a bad image. The `text()` type guard treats every non-`ReadableStream` as a `TextRasterize`.

## Where

[`src/cloudflare/internal/images-api.ts` @ `6552d999`](https://github.com/cloudflare/workerd/blob/6552d999c79ef651b5463c6d692d065a8ce6f859/src/cloudflare/internal/images-api.ts)

Guard (too wide):

https://github.com/cloudflare/workerd/blob/6552d999c79ef651b5463c6d692d065a8ce6f859/src/cloudflare/internal/images-api.ts#L16-L18

```ts
function isTextSource(source: ImageSource): source is TextRasterize {
return !(source instanceof ReadableStream);
}
```

Crash:

https://github.com/cloudflare/workerd/blob/6552d999c79ef651b5463c6d692d065a8ce6f859/src/cloudflare/internal/images-api.ts#L20-L24

```ts
function serializeTextSource(source: TextRasterize): string {
const obj = {
text: source.content,
font: source.options.font, // ArrayBuffer → options === undefined
};
```

Called from `output()`:

https://github.com/cloudflare/workerd/blob/6552d999c79ef651b5463c6d692d065a8ce6f859/src/cloudflare/internal/images-api.ts#L151-L153

`text()` is the only correct constructor for that shape:

https://github.com/cloudflare/workerd/blob/6552d999c79ef651b5463c6d692d065a8ce6f859/src/cloudflare/internal/images-api.ts#L417-L419

Types already say `ImageSource = ReadableStream | TextRasterize` and `input(stream: ReadableStream)`:

https://github.com/cloudflare/workerd/blob/6552d999c79ef651b5463c6d692d065a8ce6f859/types/defines/images.d.ts#L36
https://github.com/cloudflare/workerd/blob/6552d999c79ef651b5463c6d692d065a8ce6f859/types/defines/images.d.ts#L310-L320

JS still accepts an `ArrayBuffer` at runtime. Same guard is used for draw overlays ([#L219](https://github.com/cloudflare/workerd/blob/6552d999c79ef651b5463c6d692d065a8ce6f859/src/cloudflare/internal/images-api.ts#L219)).

## Why this is a regression

Docs still tell people to pass bytes:

- https://developers.cloudflare.com/images/optimization/binding/#transformoptions
`const bytes = await env.IMAGES.hosted.image("IMAGE_ID").bytes();` then `env.IMAGES.input(bytes)`
- https://blog.cloudflare.com/improve-your-media-pipelines-with-the-images-binding-for-cloudflare-workers/
`const fileBuffer = await file.arrayBuffer();` then `env.IMAGES.input(fileBuffer)`

`hosted.upload()` still accepts `ArrayBuffer` ([images.d.ts#L270](https://github.com/cloudflare/workerd/blob/6552d999c79ef651b5463c6d692d065a8ce6f859/types/defines/images.d.ts#L270)). `input()` used to as well.

## Repro

```js
export default {
async fetch(request, env) {
const origin = await fetch("https://cf-assets.www.cloudflare.com/dzlvafdwdttg/6Lx8xYhQ3h4bS3kYxYzYzY/x/cloudflare.png");
const buf = await origin.arrayBuffer();
return (
await env.IMAGES.input(buf).transform({ width: 200 }).output({ format: "image/webp" })
).response();
},
};
```

Expected: WebP.
Actual: `TypeError: Cannot read properties of undefined (reading 'font')`.

```js
env.IMAGES.input(new Blob([buf]).stream()) // works
```

## Suggested fix

Detect text by shape, not “not a stream”:

```ts
function isTextSource(source: ImageSource): source is TextRasterize {
return (
typeof source === "object" &&
source !== null &&
"content" in source &&
"options" in source &&
typeof source.content === "string"
);
}
```

And/or wrap bytes in `input()` so the blog + R2/`fetch().arrayBuffer()` keep working:

```ts
input(image, options) {
const stream =
image instanceof ReadableStream ? image : new Blob([image]).stream();
// existing decode / ImageTransformerImpl
}
```

If you reject bytes, throw `IMAGES_TRANSFORM_ERROR` (“`input()` expects a ReadableStream”) instead of reading `.font`.

## Workaround

`env.IMAGES.input(new Blob([buffer], { type }).stream())`.

Contributor guide

Open the contributing guide

Research direction

Start in src/cloudflare/internal/images-api.ts with isTextSource(), serializeTextSource(), input(), and output(), then compare the ImageSource and input() declarations in types/defines/images.d.ts. Reproduce the ArrayBuffer path from the issue and verify that it is no longer treated as TextRasterize; completion should preserve the documented byte-input behavior or return the intended explicit input error.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, typescript
Domain
api, backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.