DEV Community

xiaoxu
xiaoxu

Posted on

Testing Binary Image Responses Beyond HTTP 200

Testing Binary Image Responses Beyond HTTP 200

Why this matters

HTTP 200 proves that a handler returned successfully. It does not prove that a downloaded image is decodable, uses the requested format, has the declared dimensions, carries the right filename, or can be represented correctly by the client.

Binary transformation APIs often split their result across two channels: bytes in the body and metadata in headers. If those channels drift, the image can still open while the UI reports the wrong size or filename.

I tested a production Next.js image route as one body-plus-headers contract instead of treating status as the outcome.

What I built or tested

The route accepts multipart input, validates the file, resolves output format and quality, and passes bytes to a Sharp-based processor. On success it returns the encoded body plus:

  • Content-Type and Content-Disposition;
  • Cache-Control: no-store;
  • original and processed byte counts;
  • original and output formats;
  • a sanitized output filename;
  • decoded output width and height.

The browser adapter consumes those headers to construct its result object. They are not decorative diagnostics; they drive the download and result UI.

Setup

The private experiment generated a synthetic 40-by-24 PNG in memory, wrapped it in a File, and sent it to the real route handler through NextRequest:

const form = new FormData();
form.append("file", new File([input], "fixture.png", {
  type: "image/png",
}));
form.append("outputFormat", "webp");
form.append("quality", "80");

const response = await POST(new NextRequest(url, {
  method: "POST",
  body: form,
}));
Enter fullscreen mode Exit fullscreen mode

Direct handler invocation avoids starting a server while still exercising multipart parsing, validation, format resolution, processing, and response construction.

Step-by-step walkthrough

Validate declarations against independently decoded bytes:

Mermaid diagram 1

The test accepts success only when encoded bytes and declared metadata agree.

Decode the body

Do not infer format from Content-Type. Pass the returned bytes to an independent decoder:

const bytes = Buffer.from(await response.arrayBuffer());
const metadata = await sharp(bytes).metadata();

expect(metadata).toMatchObject({
  format: "webp",
  width: 40,
  height: 24,
});
Enter fullscreen mode Exit fullscreen mode

This catches a handler that sends PNG bytes with a WebP header or reports stale dimensions after resizing.

Reconcile byte counts

The original-size header should equal the uploaded file size. The processed-size header should equal the response body's actual byte length:

expect(response.headers.get("x-original-size"))
  .toBe(String(input.byteLength));
expect(response.headers.get("x-processed-size"))
  .toBe(String(bytes.byteLength));
Enter fullscreen mode Exit fullscreen mode

Avoid asserting that processed bytes must be smaller. Conversion and compression settings can legitimately increase a tiny or already optimized image.

Verify filename and policy

The response should declare both an attachment filename and the custom filename consumed by the client. Those values should agree with the requested output format.

Cache-Control: no-store is a separate contract. Correct image bytes do not prove that an intermediary or browser receives the intended retention instruction.

Exercise a non-binary error

A missing-file request returns status 400 with a JSON error. Test content type and message before attempting binary decoding. Clients need distinct parsing paths for structured errors and successful Blobs.

What went wrong

A status-only assertion would have passed even if every custom header were empty. The browser adapter would then use local fallbacks for filename, size, format, and dimensions, potentially hiding a server regression.

The reverse is also possible: headers can look correct while the body is corrupt or encoded in another format. Neither channel can validate the other unless the test decodes and reconciles them.

The experiment verified direct handler behavior, not XMLHttpRequest upload progress or a deployed proxy. An intermediary could alter Content-Disposition, expose fewer headers cross-origin, or change caching behavior. Keep one transport-level test for the deployed shape.

Fix or mitigation

Use a contract matrix:

Concern Assertion source
status response
encoded format independent body decoder
dimensions decoder compared with headers
original bytes uploaded fixture compared with header
processed bytes response buffer compared with header
filename attachment and custom filename headers
retention cache-control header
failure non-2xx content type and structured payload

Keep fixture dimensions small and deterministic. Assert relationships rather than hardcoded compression ratios. For resize routes, calculate expected dimensions from a known input and requested options.

On the client, retain sensible fallbacks for resilience, but test the server contract strictly so fallbacks do not mask regressions in CI.

Trade-offs

Direct route tests are fast and focused, but they bypass a real HTTP server, reverse proxy, CORS configuration, and browser download behavior. Pair them with a smaller end-to-end suite.

Custom headers make the client simple, yet every added field becomes versioned API surface. Prefer a minimal set that the UI actually consumes.

Independent image decoding adds native-library cost to tests. It is worthwhile at the binary boundary because string or snapshot assertions cannot validate encoded artifacts.

Exact encoded byte lengths can vary across library versions. Compare each header with the body generated in the same run rather than pinning an absolute size.

How I verified it

The production route returned a 124-byte WebP from a 150-byte generated PNG. Independent decoding reported WebP, 40-by-24 pixels.

Every metadata field matched the same artifact: content type image/webp, filename fixture.webp, original format PNG, output format WebP, width 40, height 24, and byte-count headers equal to their buffers. The cache policy was no-store.

The negative request returned 400, JSON content type, and No image file received.

Existing browser tests provide the outer evidence: they save actual download URLs and decode output format and dimensions. The direct experiment adds exact route-response reconciliation without claiming deployed proxy behavior.

Conclusion

A binary API response is not just its body, and success is not just its status.

Decode the returned artifact, compare it with every client-visible metadata field, verify filename and cache policy, and test a structured error branch. Keep browser coverage for transport-specific behavior.

That gives one answer to the only question users care about: does the downloaded file—and everything the UI says about it—describe the same real result?

Top comments (0)