didi/DoKit · error · IOException
closed
Error message
closed
What it means
The OutputStreamRequestBody's anonymous OutputStream throws IOException("closed") (deliberately IOException, not IllegalStateException, to match OkHttp's semantics) when write() is called after close(). Writing to an already-closed request-body stream corrupts accounting of bytes sent, so it is rejected.
Source
Thrown at Android/dokit/src/main/java/com/didichuxing/doraemonkit/aop/urlconnection/ObsoleteUrlFactory.java:833
boolean closed;
void initOutputStream(final BufferedSink sink, final long expectedContentLength) {
this.timeout = sink.timeout();
this.expectedContentLength = expectedContentLength;
// An output stream that writes to sink. If expectedContentLength is not -1, then this expects
// exactly that many bytes to be written.
this.outputStream = new OutputStream() {
private long bytesReceived;
@Override
public void write(int b) throws IOException {
write(new byte[]{(byte) b}, 0, 1);
}
@Override
public void write(byte[] source, int offset, int byteCount) throws IOException {
if (closed) throw new IOException("closed"); // Not IllegalStateException!
if (expectedContentLength != -1L && bytesReceived + byteCount > expectedContentLength) {
throw new ProtocolException("expected " + expectedContentLength
+ " bytes but received " + bytesReceived + byteCount);
}
bytesReceived += byteCount;
try {
sink.write(source, offset, byteCount);
} catch (InterruptedIOException e) {
throw new SocketTimeoutException(e.getMessage());
}
}
@Override
public void flush() throws IOException {
if (closed) return; // Weird, but consistent with historical behavior.
sink.flush();View on GitHub (pinned to 626827cddb)
Solutions
- Track stream state (boolean closed) in your writer and skip writes after close.
- Ensure only one component owns close(); write the complete body (including multipart trailing boundary) before closing.
- Move cleanup writes out of finally blocks; close() should be the last operation.
Example fix
// before os.write(payload); os.close(); os.write(trailer); // IOException: closed // after os.write(payload); os.write(trailer); os.close();
Defensive patterns
Strategy: try-catch
Validate before calling
boolean streamClosed = false; // your writer checks: if (streamClosed) return; before each write
Try / catch
try { os.write(chunk); } catch (IOException e) { if ("closed".equals(e.getMessage())) { /* logic bug: writing after close */ throw new IllegalStateException("body already closed", e); } throw e; } Prevention
- Single owner for close(): write everything, then close, never write again.
- Avoid writes in finally blocks and avoid double-closing helpers.
When it happens
Trigger: Calling write()/flush-with-write on the stream obtained from getOutputStream() after close() was called on it; a second flush after close that actually writes; writing in a finally block after the stream was closed on an error path.
Common situations: Multipart writers or serializers that close the stream in a helper and then write a trailing boundary; error-path cleanup code that still attempts a final write; wrappers that close and then retry on the same stream.
Related errors
- method does not support a request body: ${method}
- cannot write request body after response has been read
- ${method} does not support writing
- Unable to mark: <e>
- Cannot access request header fields after connection is set
AI-assisted analysis of didi/DoKit@626827cddb (2026-08-14).
Data as JSON: /api/errors/55bc1e7a81ce946d.
Report an issue: GitHub.