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

  1. Track stream state (boolean closed) in your writer and skip writes after close.
  2. Ensure only one component owns close(); write the complete body (including multipart trailing boundary) before closing.
  3. 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

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


AI-assisted analysis of didi/DoKit@626827cddb (2026-08-14). Data as JSON: /api/errors/55bc1e7a81ce946d. Report an issue: GitHub.