Why does OkHttp say the response body is closed?

The error appears because an OkHttp response body is a one-shot stream: it can be read once, and reading it closes it. When code tries to read the body a second time, or reads it after something else already consumed and closed it, OkHttp throws an illegal state exception complaining that the body is closed. The OkHttp response body is not a stored string waiting to be fetched repeatedly — it is a live connection to incoming bytes, and it behaves like one.

This trips up almost everyone who uses the library for the first time. The response object looks like a container holding data, so calling the reading method twice feels harmless, the way reading a variable twice would be. Underneath, though, the first read drained the stream and released the connection, and there is simply nothing left for the second read to consume.

This article explains why the body works this way, which patterns trigger the closed error, and how to handle bodies so it never happens: previewing safely, streaming large responses, and closing every body once. It also covers the quieter problem of bodies that are never closed at all, which damages an app in slow motion rather than with a crash.

A response body is a stream, not a container

When a response arrives, its body has usually not been downloaded yet. What the client holds is a stream connected to the socket, and the bytes flow as your code reads them. This design is deliberate: a two-megabyte response and a two-hundred-megabyte response cost the same memory until something decides to read, and a caller that only needs the first bytes never pays for the rest.

The trade-off is that streams move in one direction. There is no rewinding to the start, because the earlier bytes may already have been discarded after passing through buffers, decompression and decryption. Asking a stream for its contents twice is like asking a river for the same water twice — the second request arrives after the water has gone.

Methods that read the whole body must close it, previews must be taken without disturbing the stream, and code that shares a response between a logger and a parser has to cooperate, because only one of them can drink from the stream.

What reading the whole body actually does

The convenience methods that return the body as text or bytes do three things in sequence: they read the stream to its end, they convert what they read, and they close the body. Closing is not optional housekeeping they perform as a courtesy — it is how the underlying connection is finished and returned for reuse once the data has been fully consumed.

After that call returns, the body reports itself closed, and any further attempt to read from it, or to obtain its source for streaming, fails immediately with the closed error. This is the single most common cause of the exception: an innocent-looking second read, often in a different part of the code from the first.

Typical shapes of the bug are easy to recognise. A developer logs the body for debugging and then hands the same response to the parsing code. A helper checks whether the body contains an error message and then returns it for normal processing. A test prints the body, and the assertion below it reads it again. In every case the first read succeeded — which makes the crash at the second one feel baffling until the one-shot rule is known.

Why a second read fails with the closed error

The exception is a guard, not a malfunction. It fires because the alternative — silently returning empty or partial data — would produce far worse bugs downstream. A parser receiving an empty string would report a mysterious format error; an app receiving zero bytes would write a corrupt file. Failing loudly at the exact line that misuses the body points straight at the real mistake.

The wording of the message is short, usually just the word closed. It does not mean the server closed anything, and it does not mean the network dropped. It means this particular response body object has already been consumed or closed by your own code.

Once you know that, the debugging strategy changes completely. Instead of investigating the server or the connection, search your code for every place the body is touched between receiving the response and the line that crashed. The culprit is always among them — often a well-meaning log statement added days earlier.

Previewing content safely with a peek

Sometimes code legitimately needs to look at the body without owning it: logging the first characters, checking whether a payload looks like an error, or deciding which parser should handle it. For this, the body offers a peek operation that copies up to a chosen number of bytes and returns them as a separate, throwaway body, leaving the original stream untouched.

Peeking is deliberately limited. Asking for a small preview is cheap, but the peek cannot exceed the amount the stream can buffer, so it suits headers-sized questions, not whole documents. If your code finds itself wanting to peek at megabytes, what it actually wants is to consume the body and work with the result directly.

The rule of thumb is simple. Peek when you only need a glimpse and the real consumer comes later. Read fully when you are the consumer. Confusing those two roles is precisely what produces the closed error in shared code paths.

Streaming when the body is large

For downloads and other large payloads, consuming the body as text is the wrong tool even once: it loads everything into memory in a single step. The better pattern is to take the body’s byte stream or buffered source and read it in chunks, writing each chunk where it needs to go — a file, a database, or a parser that accepts incremental input.

Streaming also changes progress reporting from impossible to natural, because the code can count bytes as they pass. It is the pattern behind every sensible file download built on the client: memory use stays flat no matter how large the response grows.

A streamed body still follows the same closing rules. When the reading loop finishes — or fails halfway — the body must be closed so the connection is released. Partial reads that abandon the stream without closing it leave the connection unusable for reuse, which leads directly to the problem described next.

Closing every body, exactly once

The reliable habit is to wrap response handling in a construct that closes the body automatically: a use block in Kotlin or try-with-resources in Java. The response itself is closeable, so the construct can own the whole thing. Whichever way the block exits — normally, early return, or exception — the body is closed and the connection is dealt with.

The canonical shape is small enough to memorise:

client.newCall(request).execute().use { response ->
    if (!response.isSuccessful) throw IOException("Unexpected code $response")
    val text = response.body.string()
    println(text)
}

Notice what this structure guarantees. The status is checked before the body is read, the body is read once, and closing does not depend on the developer remembering a final step on every path. Most closed-body bugs and most connection leaks disappear when this shape becomes the default everywhere, including in callbacks and quick one-off scripts.

Exactly once matters in both directions. Reading twice crashes; closing is the mechanism that makes the second read fail. But deliberately closing a body early and then letting other code proceed as if it were fresh is the same misuse in a quieter costume.

What happens when a body is never closed

The mirror-image mistake produces no exception at all, which makes it more dangerous. If a response body is neither fully read nor closed, its connection cannot return to the pool. The connection sits occupied, holding a socket and a thread’s worth of bookkeeping.

An app that leaks a body occasionally will not notice. An app that leaks one on a common path — an error branch, an ignored response, a health check — slowly accumulates dead connections until the pool is exhausted. New calls then stall waiting for a connection that will never free up, and the symptom looks like a network or server problem rather than a missing close in the code.

Error paths are the classic hiding place. Code reads bodies carefully on success but drops the response entirely when the status is unexpected, or returns early from a function after opening a call. Auditing every early return and every ignored response for a missing close is unglamorous work that pays for itself the first time a hang is traced back to it.

How logging can consume the body by accident

The most common way a body gets read before its real consumer is logging. A verbose logging interceptor that prints the response body must read the stream, and if it simply returns the original response afterwards, the caller inherits an empty, closed body. The resulting crash then appears in innocent code, far from the logger that caused it.

Properly written logging avoids this by rebuilding what it borrowed: reading the data, then handing onwards a fresh body containing the same bytes. That is one reason to prefer the library’s own well-tested logging support over a quick hand-written version, and to keep verbose body logging out of release builds entirely.

If the closed error started appearing right after logging was added or its level was raised, that timing is not a coincidence. Check what the logger does with bodies before suspecting anything else; in practice it is the explanation more often than not.

Error bodies and checking the status first

Failed responses have bodies too, usually carrying a server-provided explanation, and they follow identical rules: one read, then closed. Code that checks whether a call succeeded before reading anything avoids the awkward pattern of reading the body speculatively and then discovering it needed the error text instead — a pattern that spends the single read on the wrong branch.

The error body deserves the same careful ownership as a success body. Reading it for a message, logging it, and closing it should happen in one place. Letting it drop unread and unclosed leaks the connection, and error paths under load are precisely where an app can least afford a shrinking pool.

One related habit helps structure all of this: decide the owner of the response at the moment the call returns. Whether calls in your codebase are made with make OkHttp calls synchronously or through callbacks, a single owner that reads once and always closes turns the whole family of body mistakes into something that can only happen by breaking one visible convention.

Frequently Asked Questions

If I read the body into a variable, can I use that as often as I like?

Yes. The one-shot restriction applies to the body stream, not to the string or bytes you obtained from it. Reading once into a variable and then logging, parsing and storing that variable is the intended pattern. The mistake is asking the body itself twice.

Does reading only part of the body and stopping cause problems?

It can. If you stop early, close the body explicitly so the connection is released. Abandoning a partially read body without closing it strands the connection, and repeated partial reads are a reliable way to exhaust the connection pool over time.

Why does the error sometimes appear inside a callback instead?

Because the same misuse can happen on the asynchronous path. If something upstream of the callback — a logger, an interceptor, a debugging helper — already consumed the body, the callback receives a closed body and fails on its first read. The location of the crash marks where the problem surfaced, not where it was caused.

Can I save the body now and read it after the response is closed?

No. Closing ends the body’s usefulness, and buffered content cannot be retrieved afterwards. If data is needed later, read it fully while the response is open and keep the resulting string or bytes. For large content, stream it to a file during the read and work with the file afterwards.

Conclusion

The closed error is OkHttp enforcing a simple contract: a response body is a stream that supports exactly one read, after which it is closed and its connection is dealt with. Double reads, speculative reads and logging that borrows the stream all break that contract, and unclosed bodies break its other half by quietly starving the connection pool.

The fix is a habit rather than a technique — one owner per response, a status check first, a single full read or a deliberate peek, and a use block that guarantees closing on every path. Adopt that shape everywhere and the exception stops occurring.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top