Java ImageIO.read returns null on a Sume WebP: request PNG
ImageIO.read gives null when no reader claims the stream. Check the reader list, then pin output_format to png or jpeg on a Sume /v1/images call.

ImageIO.read returns null, not an exception, when no registered reader recognises the bytes. Oracle's ImageIO documentation says exactly that, and it also gives getReaderFormatNames() to list what your runtime can decode. So a downloaded Sume image that comes back null is usually a WebP that your JDK has no reader plugin for. Whether WebP is on your list depends on the JDK and any plugins on the classpath, so print the list instead of assuming.
The fix on the Sume side is one field. The image API docs list output_format as png, jpeg or webp, and the catalog restricts it per model. Recraft V4 is the exception: its catalog row lists only webp, so a Java client that must read Recraft output needs a WebP reader or a different model.
How do I tell a null from a bad download?
Separate the two failures. A failed or truncated download throws an IOException or leaves you with an empty byte array. A complete download of a format with no reader gives null. Check the bytes first, then the null.
Every /v1/images response carries data[].media_type, for example image/webp, next to data[].url. Log it. If it says image/webp and your reader list lacks webp, you have found the cause without a debugger.
A single-file check you can run
Save this as ReadSume.java and run java ReadSume.java <image-url> on JDK 11 or later, passing the data[0].url from a Sume response. It downloads the bytes, tries ImageIO.read, and prints either the size or the registered readers.
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import javax.imageio.ImageIO;
public class ReadSume {
public static void main(String[] args) throws Exception {
HttpClient http = HttpClient.newHttpClient();
HttpRequest get = HttpRequest.newBuilder(URI.create(args[0])).build();
byte[] bytes = http.send(get, HttpResponse.BodyHandlers.ofByteArray()).body();
BufferedImage img = ImageIO.read(new ByteArrayInputStream(bytes));
if (img == null) {
System.out.println("No reader claimed " + bytes.length + " bytes. Registered: "
+ String.join(", ", ImageIO.getReaderFormatNames()));
return;
}
System.out.println(img.getWidth() + "x" + img.getHeight());
}
}Ask for a format Java reads
Send output_format in the request body so the decision is yours and not the model default. png is lossless and fine for graphics; jpeg is smaller for photos. Both are accepted by every catalog model except Recraft V4. If a model does not list the parameter you send, the API answers 400 unsupported_parameter instead of ignoring it, so a wrong value fails loudly.
A body that works for the Java check above:
{
"model": "openai/gpt-image-2.5",
"prompt": "a red kettle on a white table, studio light",
"quality": "low",
"output_format": "png"
}Do not forget the 202 path
POST /v1/images waits up to 30 seconds and returns 200 with the image body. If the job needs longer, you get 202 and a job envelope with status_url and result_url, and the image comes from the standard job result, not from the response. Branch on HttpResponse.statusCode() before you touch data[0].url. The jobs guide says to poll with backoff and never to resubmit a paid request because a local timeout fired.
Sources
Related posts
More in Developers
- Start the trim step from a job.completed webhook: Python verifier
Let Sume's signed job.completed webhook trigger the next chain step. A stdlib Python verifier that refuses an empty secret, plus dedupe on job_id.
- Job id or run id: which Sume endpoint and helper do you poll?
A job_ id is polled at /v1/jobs/:id with waitForJob. An arun_ id is a Format, Action or Agent run, polled with waitForRun and a family argument.
- job_id, request_id, Idempotency-Key: which one goes in which column
Your Idempotency-Key is the unique key before submit, job_id or run_id is the poll key, and request_id dedupes webhooks and goes into support tickets.
- 503 status_busy on GET /v1/jobs/{id}/status: back off and jitter
status_busy means Sume's job status read gate is full. Reads of the same job are shared; the cap is 100 distinct in-flight reads. Poll slower, add jitter.
Written by Sume