diff --git a/src/cloudflare/internal/images-api.ts b/src/cloudflare/internal/images-api.ts index efe33b99174..6e1e1845c8d 100644 --- a/src/cloudflare/internal/images-api.ts +++ b/src/cloudflare/internal/images-api.ts @@ -60,12 +60,11 @@ class TransformationResultImpl implements ImageTransformationResult { : stream; } - response(): Response { - return new Response(this.image(), { - headers: { - 'content-type': this.contentType(), - }, - }); + response(options?: ImageTransformationResponseOptions): Response { + const headers = new Headers(options?.headers); + headers.set('content-type', this.contentType()); + + return new Response(this.image(), { headers }); } } diff --git a/src/cloudflare/internal/images.d.ts b/src/cloudflare/internal/images.d.ts index 280061e03e9..f8c0af016c0 100644 --- a/src/cloudflare/internal/images.d.ts +++ b/src/cloudflare/internal/images.d.ts @@ -267,11 +267,16 @@ type ImageTransformationOutputOptions = { encoding?: 'base64'; }; +type ImageTransformationResponseOptions = { + headers?: HeadersInit; +}; + interface ImageTransformationResult { /** * The image as a response, ready to store in cache or return to users + * @param options Options that apply to the returned response, e.g. additional headers */ - response(): Response; + response(options?: ImageTransformationResponseOptions): Response; /** * The content type of the returned image */ diff --git a/src/cloudflare/internal/test/images/images-api-instrumentation-test.js b/src/cloudflare/internal/test/images/images-api-instrumentation-test.js index 6f7f5698c23..d5a0f2f75c4 100644 --- a/src/cloudflare/internal/test/images/images-api-instrumentation-test.js +++ b/src/cloudflare/internal/test/images/images-api-instrumentation-test.js @@ -416,6 +416,60 @@ const expectedSpans = [ 'http.response.body.size': 359n, closed: true, }, + { + name: 'images_output', + 'cloudflare.binding.type': 'Images', + 'cloudflare.images.options.format': 'image/avif', + closed: true, + }, + { + name: 'fetch', + 'network.protocol.name': 'http', + 'network.protocol.version': 'HTTP/1.1', + 'http.request.method': 'POST', + 'url.full': 'https://js.images.cloudflare.com/transform', + 'http.request.header.content-type': + 'multipart/form-data; boundary=', + 'http.response.status_code': 200n, + 'http.response.body.size': 60n, + closed: true, + }, + { + name: 'images_output', + 'cloudflare.binding.type': 'Images', + 'cloudflare.images.options.format': 'image/avif', + closed: true, + }, + { + name: 'fetch', + 'network.protocol.name': 'http', + 'network.protocol.version': 'HTTP/1.1', + 'http.request.method': 'POST', + 'url.full': 'https://js.images.cloudflare.com/transform', + 'http.request.header.content-type': + 'multipart/form-data; boundary=', + 'http.response.status_code': 200n, + 'http.response.body.size': 60n, + closed: true, + }, + { + name: 'images_output', + 'cloudflare.binding.type': 'Images', + 'cloudflare.images.options.format': 'image/avif', + closed: true, + }, + { + name: 'fetch', + 'network.protocol.name': 'http', + 'network.protocol.version': 'HTTP/1.1', + 'http.request.method': 'POST', + 'url.full': 'https://js.images.cloudflare.com/transform', + 'http.request.header.content-type': + 'multipart/form-data; boundary=', + 'http.response.status_code': 200n, + 'http.response.body.size': 60n, + closed: true, + }, { name: 'images_output', 'cloudflare.binding.type': 'Images', diff --git a/src/cloudflare/internal/test/images/images-api-test.js b/src/cloudflare/internal/test/images/images-api-test.js index 00e9de64de2..08168c13b8d 100644 --- a/src/cloudflare/internal/test/images/images-api-test.js +++ b/src/cloudflare/internal/test/images/images-api-test.js @@ -130,6 +130,79 @@ export const test_images_transform = { }, }; +export const test_images_response_default_headers = { + /** + * @param {unknown} _ + * @param {Env} env + */ + async test(_, env) { + const result = await env.images + .input(inputStream(['png'])) + .output({ format: 'image/avif' }); + + const response = result.response(); + + assert.strictEqual( + response.headers.get('content-type'), + 'application/json' + ); + assert.strictEqual(response.headers.get('cache-control'), null); + }, +}; + +export const test_images_response_custom_headers = { + /** + * @param {unknown} _ + * @param {Env} env + */ + async test(_, env) { + const result = await env.images + .input(inputStream(['png'])) + .output({ format: 'image/avif' }); + + const response = result.response({ + headers: { + 'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400', + 'Cache-Tag': 'my-tag', + }, + }); + + assert.strictEqual( + response.headers.get('cache-control'), + 'public, max-age=3600, stale-while-revalidate=86400' + ); + assert.strictEqual(response.headers.get('cache-tag'), 'my-tag'); + // content-type still reflects the transformed image + assert.strictEqual( + response.headers.get('content-type'), + 'application/json' + ); + }, +}; + +export const test_images_response_content_type_not_overridable = { + /** + * @param {unknown} _ + * @param {Env} env + */ + async test(_, env) { + const result = await env.images + .input(inputStream(['png'])) + .output({ format: 'image/avif' }); + + // Attempting to set a conflicting content-type + // the transformed image's actual content type always wins. + const response = result.response({ + headers: { 'Content-Type': 'text/plain' }, + }); + + assert.strictEqual( + response.headers.get('content-type'), + 'application/json' + ); + }, +}; + export const test_images_nested_draw = { /** * @param {unknown} _ diff --git a/types/defines/images.d.ts b/types/defines/images.d.ts index caf381f6afb..20df25ec2d9 100644 --- a/types/defines/images.d.ts +++ b/types/defines/images.d.ts @@ -264,11 +264,16 @@ type ImageTransformationOutputOptions = { encoding?: 'base64'; }; +type ImageTransformationResponseOptions = { + headers?: HeadersInit; +}; + interface ImageTransformationResult { /** * The image as a response, ready to store in cache or return to users + * @param options Options that apply to the returned response, e.g. additional headers */ - response(): Response; + response(options?: ImageTransformationResponseOptions): Response; /** * The content type of the returned image */ diff --git a/types/generated-snapshot/experimental/index.d.ts b/types/generated-snapshot/experimental/index.d.ts index 05c9f61bea5..b715fef192c 100755 --- a/types/generated-snapshot/experimental/index.d.ts +++ b/types/generated-snapshot/experimental/index.d.ts @@ -14836,11 +14836,15 @@ interface ImageTransformer { type ImageTransformationOutputOptions = { encoding?: "base64"; }; +type ImageTransformationResponseOptions = { + headers?: HeadersInit; +}; interface ImageTransformationResult { /** * The image as a response, ready to store in cache or return to users + * @param options Options that apply to the returned response, e.g. additional headers */ - response(): Response; + response(options?: ImageTransformationResponseOptions): Response; /** * The content type of the returned image */ diff --git a/types/generated-snapshot/experimental/index.ts b/types/generated-snapshot/experimental/index.ts index 92867d33f75..de737362f4b 100755 --- a/types/generated-snapshot/experimental/index.ts +++ b/types/generated-snapshot/experimental/index.ts @@ -14863,11 +14863,15 @@ export interface ImageTransformer { export type ImageTransformationOutputOptions = { encoding?: "base64"; }; +export type ImageTransformationResponseOptions = { + headers?: HeadersInit; +}; export interface ImageTransformationResult { /** * The image as a response, ready to store in cache or return to users + * @param options Options that apply to the returned response, e.g. additional headers */ - response(): Response; + response(options?: ImageTransformationResponseOptions): Response; /** * The content type of the returned image */ diff --git a/types/generated-snapshot/index.d.ts b/types/generated-snapshot/index.d.ts index 12246afaf8d..9a0498b0407 100755 --- a/types/generated-snapshot/index.d.ts +++ b/types/generated-snapshot/index.d.ts @@ -14188,11 +14188,15 @@ interface ImageTransformer { type ImageTransformationOutputOptions = { encoding?: "base64"; }; +type ImageTransformationResponseOptions = { + headers?: HeadersInit; +}; interface ImageTransformationResult { /** * The image as a response, ready to store in cache or return to users + * @param options Options that apply to the returned response, e.g. additional headers */ - response(): Response; + response(options?: ImageTransformationResponseOptions): Response; /** * The content type of the returned image */ diff --git a/types/generated-snapshot/index.ts b/types/generated-snapshot/index.ts index 39357652538..006039e12a8 100755 --- a/types/generated-snapshot/index.ts +++ b/types/generated-snapshot/index.ts @@ -14215,11 +14215,15 @@ export interface ImageTransformer { export type ImageTransformationOutputOptions = { encoding?: "base64"; }; +export type ImageTransformationResponseOptions = { + headers?: HeadersInit; +}; export interface ImageTransformationResult { /** * The image as a response, ready to store in cache or return to users + * @param options Options that apply to the returned response, e.g. additional headers */ - response(): Response; + response(options?: ImageTransformationResponseOptions): Response; /** * The content type of the returned image */ diff --git a/types/test/types/images.ts b/types/test/types/images.ts new file mode 100644 index 00000000000..80a8fb52838 --- /dev/null +++ b/types/test/types/images.ts @@ -0,0 +1,35 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +function expectType(_value: T) {} + +export const handler: ExportedHandler<{ IMAGES: ImagesBinding }> = { + async fetch(request, env) { + const result = await env.IMAGES.input(request.body!).output({ + format: 'image/webp', + }); + + // response() can be called with no arguments, as before. + expectType(result.response()); + + // response() accepts additional headers, e.g. for cache control. + expectType( + result.response({ + headers: { + 'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400', + }, + }) + ); + + // headers can also be a Headers instance or an iterable of pairs. + expectType( + result.response({ headers: new Headers({ 'Cache-Tag': 'my-tag' }) }) + ); + expectType( + result.response({ headers: [['Cache-Tag', 'my-tag']] }) + ); + + return result.response(); + }, +};