Fix Response constructor regression for direct PSR-7 stream bodies#25
Fix Response constructor regression for direct PSR-7 stream bodies#25
Response constructor regression for direct PSR-7 stream bodies#25Conversation
Agent-Logs-Url: https://github.com/voku/httpful/sessions/779842a8-4185-4102-9996-2a2a2b0cfd4a Co-authored-by: voku <264695+voku@users.noreply.github.com>
Agent-Logs-Url: https://github.com/voku/httpful/sessions/779842a8-4185-4102-9996-2a2a2b0cfd4a Co-authored-by: voku <264695+voku@users.noreply.github.com>
Agent-Logs-Url: https://github.com/voku/httpful/sessions/779842a8-4185-4102-9996-2a2a2b0cfd4a Co-authored-by: voku <264695+voku@users.noreply.github.com>
Agent-Logs-Url: https://github.com/voku/httpful/sessions/779842a8-4185-4102-9996-2a2a2b0cfd4a Co-authored-by: voku <264695+voku@users.noreply.github.com>
Agent-Logs-Url: https://github.com/voku/httpful/sessions/510ea229-e42c-4cda-93b7-083f613bb6e4 Co-authored-by: voku <264695+voku@users.noreply.github.com>
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request updates the Response constructor to use StreamInterface and introduces a mechanism to preserve the body stream when specific conditions are met, such as the absence of a request context. It also adds a comprehensive set of tests in OutputContractCoverageTest.php and ResponseTest.php. The review feedback recommends renaming the new private method to _shouldPreserveBodyStream to follow the repository's naming conventions and suggests adding a check for raw_body to ensure the preservation logic correctly identifies original stream inputs.
| $this->_interpretHeaders(); | ||
|
|
||
| $bodyParsed = $this->_parse($body); | ||
| $bodyParsed = $this->shouldPreserveBodyStream($body) ? $body : $this->_parse($body); |
| return Setup::setupGlobalMimeType($parse_with)->parse((string) $body); | ||
| } | ||
|
|
||
| private function shouldPreserveBodyStream($body): bool |
There was a problem hiding this comment.
In this repository, private and internal methods consistently use a leading underscore (e.g., _parse, _interpretHeaders). To adhere to this implicit style guide, please rename this method to _shouldPreserveBodyStream.
private function _shouldPreserveBodyStream($body): boolReferences
- Private and internal methods in this repository follow a convention of being prefixed with an underscore. (link)
| return $body instanceof StreamInterface | ||
| && $this->request === null |
There was a problem hiding this comment.
The current implementation might inadvertently skip the parsing path for non-stream inputs (like strings or arrays) that are converted to streams earlier in the constructor (lines 95-98). This would cause raw_body to be overwritten with a StreamInterface object at line 134, which changes the expected behavior of getRawBody() for those types.
Adding a check for $this->raw_body === null ensures that this preservation logic only applies when the input was originally a stream (or null), as raw_body is only populated at line 96 when the input is not a stream.
return $body instanceof StreamInterface
&& $this->raw_body === null
&& $this->request === nullThere was a problem hiding this comment.
Pull request overview
This PR fixes a regression in Httpful\Response where constructing a response directly from a PSR-7 StreamInterface could eagerly stringify/parse the stream during construction, replacing the original body instance.
Changes:
- Treat any PSR-7
StreamInterfaceas a first-class response body input inResponse::__construct(). - Add a regression test ensuring
new Response($stream)preserves the same stream instance and does not call__toString()during construction. - Add broader output/contract coverage tests for several behaviors across Factory/Setup/Handlers/Response/ServerRequest.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/Httpful/Response.php |
Updates constructor logic to preserve direct PSR-7 stream bodies when there’s no parsing context. |
tests/Httpful/ResponseTest.php |
Adds a regression test asserting generic StreamInterface bodies are preserved and not stringified during construction. |
tests/Httpful/OutputContractCoverageTest.php |
Introduces additional tests to exercise various output/contract behaviors for coverage. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| $bodyParsed = $this->_parse($body); | ||
| $bodyParsed = $this->shouldPreserveBodyStream($body) ? $body : $this->_parse($body); | ||
| $this->body = Stream::createNotNull($bodyParsed); |
There was a problem hiding this comment.
When shouldPreserveBodyStream() returns true, $bodyParsed can be an arbitrary PSR-7 StreamInterface. Passing that into Stream::createNotNull() will hit Stream::createNotNull()’s assert($stream instanceof self) and can raise an assertion warning/exception (PHPUnit converts warnings to exceptions), breaking the new regression test and any runtime with assertions enabled. Consider bypassing createNotNull() when $bodyParsed is already a StreamInterface (assign it directly), or adjust createNotNull() to only assert StreamInterface rather than self.
| $this->body = Stream::createNotNull($bodyParsed); | |
| $this->body = $bodyParsed instanceof StreamInterface | |
| ? $bodyParsed | |
| : Stream::createNotNull($bodyParsed); |
| public function testConstructorPreservesGenericStreamBodyWithoutParsingContext() | ||
| { | ||
| $body = $this->getMockBuilder(StreamInterface::class)->getMock(); | ||
| $body->expects(static::never()) | ||
| ->method('__toString'); | ||
|
|
||
| $r = new Response($body); | ||
| static::assertSame($body, $r->getBody()); | ||
| } |
There was a problem hiding this comment.
This test mocks StreamInterface and constructs new Response($body). With the current constructor implementation, $bodyParsed will be the mock and then Stream::createNotNull($bodyParsed) triggers assert($stream instanceof self) inside Stream::createNotNull(), which can fail (and PHPUnit will treat the resulting warning as an exception). Once the constructor avoids calling createNotNull() for an already-StreamInterface body (or createNotNull() is relaxed), the test will behave as intended.
| @@ -129,7 +129,7 @@ public function __construct( | |||
|
|
|||
| $this->_interpretHeaders(); | |||
|
|
|||
| $bodyParsed = $this->_parse($body); | |||
| $bodyParsed = $this->shouldPreserveBodyStream($body) ? $body : $this->_parse($body); | |||
| $this->body = Stream::createNotNull($bodyParsed); | |||
| $this->raw_body = $bodyParsed; | |||
| } | |||
There was a problem hiding this comment.
shouldPreserveBodyStream($body) is evaluated after non-stream inputs have already been coerced into a StreamInterface via Stream::create(). That means responses like new Response('baz') / new Response(null) will now also skip _parse() whenever there’s no request and no headers, changing getRawBody() (and potentially __toString() fallback behavior) from the previously-parsed scalar to a stream instance. If the intent is only to preserve original stream inputs, capture a $bodyWasStream = $body instanceof StreamInterface before coercion (or similar) and gate the preservation logic on that flag so scalar inputs keep the old parsing/raw_body semantics.
|
@copilot apply changes based on the comments in this thread PS: let's revert the code changes and rework it the TDD way |
Agent-Logs-Url: https://github.com/voku/httpful/sessions/bfb4b7d4-bf62-431a-89bf-e623e1f2dce2 Co-authored-by: voku <264695+voku@users.noreply.github.com>
Addressed in 2278465. I reverted the earlier constructor change, reintroduced the fix TDD-first with a failing regression test, and reworked |
The branch had added coverage around output contracts, but it was not exercising a real break. A direct
Responseconstruction with a generic PSR-7StreamInterfacewas still regressing by eagerly parsing/stringifying the stream instead of preserving it as the response body.Regression addressed
new Response($stream)only treatedHttpful\Streamas a first-class stream input.StreamInterfacewas coerced through the parsing path, which could invoke__toString()during construction and replace the original body instance.Behavior change
Responsenow recognizes any PSR-7StreamInterfaceas a direct body input.Coverage added
Responsewith a mockedStreamInterfaceand asserts:__toString()is not called during constructionThis change is