didi/DoKit · error · IllegalStateException

closed

Error message

closed

What it means

ByteCountBufferedSinkV3.write(byte[], int, int) checks isOpen() before writing; once close() has been called (delegating to the underlying buffered sink), isOpen() returns false and any further byte-array write throws IllegalStateException('closed'). This matches okio's BufferedSink contract that a closed sink rejects writes.

Source

Thrown at Android/dokit-okhttp-v3/src/main/java/com/didichuxing/doraemonkit/okhttp_api/ByteCountBufferedSinkV3.java:46

        this.mOriginalSink = sink;
        this.mDelegate = Okio.buffer(mOriginalSink);
        this.mByteCount = byteCount;
    }

    @Override
    public long writeAll(Source source) throws IOException {
        if (source == null) throw new IllegalArgumentException("source == null");
        long totalBytesRead = 0;
        for (long readCount; (readCount = source.read(buffer(), mByteCount)) != -1; ) {
            totalBytesRead += readCount;
            emitCompleteSegments();
        }
        return totalBytesRead;
    }

    @Override
    public BufferedSink write(byte[] source, int offset, int byteCount) throws IOException {
        if (!isOpen()) throw new IllegalStateException("closed");
        //计算出要写入的次数
        long count = (long) Math.ceil((double) source.length / mByteCount);
        for (int i = 0; i < count; i++) {
            //让每次写入的字节数精确到mByteCount 分多次写入
            long newOffset = i * mByteCount;
            long writeByteCount = Math.min(mByteCount, source.length - newOffset);
            buffer().write(source, (int) newOffset, (int) writeByteCount);
            emitCompleteSegments();
        }
        return this;
    }

    @Override
    public BufferedSink emitCompleteSegments() throws IOException {
        final Buffer buffer = buffer();
        mOriginalSink.write(buffer, buffer.size());
        return this;
    }

View on GitHub (pinned to 626827cddb)

Solutions

  1. Track lifecycle: only close the sink after all writes are complete (close in the interceptor's finally after the response is fully consumed)
  2. Check isOpen() before writing and skip/recreate the sink if closed
  3. Create a new ByteCountBufferedSinkV3 per request/response instead of reusing a closed instance

Example fix

// before
try { byteCountSink.write(body, 0, body.length); }
finally { byteCountSink.close(); }
// later, on a retry path:
byteCountSink.write(body, 0, body.length); // IllegalStateException

// after
if (byteCountSink.isOpen()) {
  byteCountSink.write(body, 0, body.length);
} else {
  byteCountSink = new ByteCountBufferedSinkV3(sink, CHUNK);
  byteCountSink.write(body, 0, body.length);
}
Defensive patterns

Strategy: validation

Validate before calling

if (byteCountSink.isOpen()) { byteCountSink.write(source, 0, source.length); }

Try / catch

try { sink.write(bytes, 0, bytes.length); } catch (IllegalStateException e) { if ("closed".equals(e.getMessage())) { /* recreate sink or skip: body already flushed */ } else throw e; }

Prevention

When it happens

Trigger: Calling write(byte[], offset, byteCount) after close() — e.g. an interceptor that copies a body to both the network and DoraomonKit's capture sink closes the sink on one path, then a retry/redirect path writes again. Also closing in a finally block but continuing to use the sink on success paths.

Common situations: OkHttp interceptor chains with retries/redirects re-invoking body copying. Exception paths where close() runs early (try-with-resources scoping too wide) and subsequent code still writes. Sharing one capture sink across multiple requests.

Related errors


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