didi/DoKit · error · ProtocolException

expected " + expectedContentLength + " bytes but received "…

Error message

expected " + expectedContentLength + " bytes but received " + bytesReceived + byteCount

What it means

In fixed-length mode (expectedContentLength != -1, set via setFixedLengthStreamingMode), the body OutputStream counts written bytes and throws ProtocolException("expected N bytes but received M") as soon as writes would exceed the declared length. The declared content length and the actual bytes written must match exactly.

Solutions

  1. Recompute the declared length to include every byte (boundaries, CRLFs, headers inside the body) — build the body into a ByteArrayOutputStream first and use its size().
  2. If the exact size is hard to know, switch to setChunkedStreamingMode().
  3. Write the body once from a single code path to avoid duplicate writes.

Example fix

// before
conn.setFixedLengthStreamingMode(file.length());
os.write(multipartHeader); os.write(fileBytes); // exceeds file.length()

// after
byte[] body = buildMultipart(file); // header+file+boundary
conn.setFixedLengthStreamingMode(body.length);
os.write(body);
Defensive patterns

Strategy: validation

Validate before calling

byte[] body = buildBody(); // exact bytes you will write
conn.setFixedLengthStreamingMode(body.length);
// then write exactly body

Prevention

When it happens

Trigger: Calling setFixedLengthStreamingMode(n) but writing more than n bytes — e.g. declaring file.length() then adding headers/boundaries on top, or writing the body twice.

Common situations: Multipart uploads where the declared length omits boundary bytes; declaring a compressed/inflated length different from the actual bytes; off-by-one or duplicated chunk writes.

Related errors


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

Appendix: source

Thrown at Android/dokit/src/main/java/com/didichuxing/doraemonkit/aop/urlconnection/ObsoleteUrlFactory.java:836

            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();
                }

                @Override

View on GitHub (pinned to 626827cddb)