Skip to content

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

Description

@Illyism

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

Guard (too wide):

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

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

Crash:

function serializeTextSource(source: TextRasterize): string {
const obj: Record<string, string | number | { url: string }> = {
text: source.content,
font: source.options.font,
};

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

Called from output():

if (isTextSource(this.#source)) {
span.setAttribute('cloudflare.images.canvas.type', 'text');
formData.append('text_input', serializeTextSource(this.#source));

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

text(content: string, options: TextOptions): ImageTransformer {
return new ImageTransformerImpl(this.#fetcher, { content, options });
}

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

type ImageSource = ReadableStream<Uint8Array> | TextRasterize;

input(
stream: ReadableStream<Uint8Array>,
options?: ImageInputOptions
): ImageTransformer;
/**
* Begin applying a series of transformations to text
* @param content string to be rendered
* @param options font, optional color and size to use in rendering text
* @returns A transform handle
*/
text(content: string, options: TextOptions): ImageTransformer;

JS still accepts an ArrayBuffer at runtime. Same guard is used for draw overlays (#L219).

Why this is a regression

Docs still tell people to pass bytes:

hosted.upload() still accepts ArrayBuffer (images.d.ts#L270). input() used to as well.

Repro

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').

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

Suggested fix

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

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:

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()).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions