In PSR-7, why can a second call to $request->getBody()->getContents() return an empty string?
answer
- the stream has a cursor
- getContents() reads the remaining bytes
- __toString() seeks to the start first
- rewind() fails on non-seekable streams
- StreamInterface is not immutable
basics
~20 sStreamInterface wraps a PHP stream with a cursor. getContents() returns only the remaining bytes, so after one full read the cursor is at the end and the next call returns ''. Call rewind() first or cast with (string).
solid answer
~40 sA PSR-7 body is a `StreamInterface`, a wrapper around a PHP stream that has a read position. `getContents()` returns the contents from the current position to the end, so the first call moves the cursor to the end and a second call returns an empty string. `__toString()` behaves differently: it MUST attempt to seek to the beginning and read everything, and it must not throw. So either call `rewind()` before re-reading or cast the body with `(string)`. `rewind()` throws a `RuntimeException` on a non-seekable stream, such as one over a socket, which is why code that needs the body twice should check `isSeekable()` or copy it into a new stream. The cursor is shared by every holder of the stream, because `StreamInterface` is the one mutable part of a PSR-7 message.
code
php · 17 lines<?php
declare(strict_types=1);
use Psr\Http\Message\StreamInterface;
function readTwice(StreamInterface $body): array
{
$first = $body->getContents(); // cursor moves to the end
$second = $body->getContents(); // '' : nothing left to read
if ($body->isSeekable()) {
$body->rewind(); // back to position 0
}
$third = $body->getContents(); // full contents again, if seekable
return [$first, $second, $third];
}go deeper
Remember that a PSR-7 body is a stream with a position: getContents() reads what is left, and rewind() or a string cast starts again from the beginning.
Explain the difference between getContents() and __toString(), what isSeekable() tells you, and why the body is the one mutable part of a PSR-7 message.
Design middleware that reads bodies once, streams large payloads in chunks, and replaces bodies with withBody() instead of writing into streams shared with other code.
Weigh streaming bodies against buffering them in memory for a service that proxies uploads: memory ceilings, retryability of non-seekable streams and simpler code.
## The symptom A middleware logs the request body, then the handler decodes it and gets an empty string: ```php $logger->debug($request->getBody()->getContents()); // reads to the end $data = json_decode($request->getBody()->getContents()); // '' -> null ``` Nothing is wrong with the request. The body object is a **stream**, and the first read moved its position to the end. ## StreamInterface in brief PSR-7 does not keep message bodies as strings, because a body can be very large and holding it all in memory is wasteful. Instead the body is a `StreamInterface`, which typically wraps a PHP stream resource such as `php://temp`, `php://input` or an open file. Like any PHP stream it has a **cursor**, the position of the next read or write. The interface exposes: - **capability checks**: `isReadable()`, `isWritable()`, `isSeekable()`; - **cursor control**: `tell()`, `eof()`, `seek($offset, $whence = SEEK_SET)`, `rewind()`; - **I/O**: `read($length)`, `write($string)`, `getContents()`; - **lifecycle**: `close()`, `detach()` (returns the underlying resource or `null`), `getSize()` (bytes or `null` if unknown), `getMetadata()`. ## getContents() versus __toString() | Method | Starts reading from | Throws on failure | |---|---|---| | `getContents()` | the **current** cursor position | yes, `RuntimeException` | | `(string) $stream` / `__toString()` | attempts to **seek to 0** first | no, it must not throw | | `read($length)` | the current position, up to `$length` bytes | yes, `RuntimeException` | So the two fixes are: 1. call `$body->rewind()` before each full read, or 2. use `(string) $body`, which rewinds when it can. The specification warns that `__toString()` may load a large amount of data into memory, and because it cannot throw, implementations typically return an empty string when reading fails. On a big download, reading in chunks with `read()` in a loop until `eof()` is the memory-safe option. ## Non-seekable streams Some streams only move forward: a socket, a pipe or a callback-based stream. On those, `isSeekable()` returns `false`, `rewind()` and `seek()` throw a `RuntimeException`, and `__toString()` cannot go back either. If code needs the content twice, read it once, keep the string, and if the message must travel on with its body intact, attach a fresh stream with `withBody()`, for example one created by a PSR-17 `StreamFactoryInterface::createStream($contents)`. ## Why the body is mutable The rest of a PSR-7 message is immutable, but `StreamInterface` is not, and the specification explains why: a stream wraps a resource, and any code holding that resource can change its state, its cursor or its content. Making the wrapper immutable would not stop that. Consequences: - the cursor position is shared by every message and every piece of code holding the same stream object; - `write()` on a response body changes that response in place; - the PSR-7 recommendation is to use **read-only** streams for server-side requests and client-side responses. When in doubt, create a new stream and attach it instead of writing into one that other code may hold. ## Practical rules - Read a body once and keep the string if you need it again. - If you must re-read, check `isSeekable()` and call `rewind()` first. - Prefer `(string)` only where a silent empty string on failure is acceptable. - Stream large bodies with `read()` and `eof()` instead of loading them whole. - Replace a body with `withBody()` and a new stream, not by writing into the old one. ## Reading in chunks For large bodies, a loop keeps memory flat regardless of size: ```php $body = $response->getBody(); while (!$body->eof()) { $chunk = $body->read(8192); // up to 8 KiB; fewer bytes are possible fwrite($out, $chunk); } ``` `read($length)` returns up to `$length` bytes and may return fewer, or an empty string when nothing is available yet, so the loop condition is `eof()`, not the chunk size. `getSize()` can help decide between buffering and streaming, but it returns `null` when the size is unknown, as it often is for a network stream.
- Why must StreamInterface::__toString() not throw?It is PHP's string-casting hook, and the PSR-7 interface was written to conform to PHP's string casting operations, so the specification forbids exceptions there. Implementations typically return an empty string on failure, which is why code that must detect read errors should use `getContents()` or `read()` instead.
- How do you safely replace a response body inside a PSR-15 middleware?Create a new stream, for example with a PSR-17 `StreamFactoryInterface::createStream($newContent)`, and return `$response->withBody($stream)`. Writing into `$response->getBody()` can leave stale bytes when the new content is shorter, fails on a read-only stream, and changes a stream other code may share.
saying these in an interview costs you the question
- Believes getContents() always returns the whole body regardless of position.
- Thinks rewind() works on every stream, including sockets and pipes.
- Assumes the body stream is immutable like the headers.
- Relies on (string) $body to surface read errors as exceptions.
- Loads multi-gigabyte bodies with getContents() instead of reading chunks.