Skip to content

Fix Response constructor regression for direct PSR-7 stream bodies#25

Open
Copilot wants to merge 6 commits intomasterfrom
copilot/increase-code-coverage-validation
Open

Fix Response constructor regression for direct PSR-7 stream bodies#25
Copilot wants to merge 6 commits intomasterfrom
copilot/increase-code-coverage-validation

Conversation

Copy link
Copy Markdown

Copilot AI commented Apr 26, 2026

The branch had added coverage around output contracts, but it was not exercising a real break. A direct Response construction with a generic PSR-7 StreamInterface was still regressing by eagerly parsing/stringifying the stream instead of preserving it as the response body.

  • Regression addressed

    • new Response($stream) only treated Httpful\Stream as a first-class stream input.
    • Any other StreamInterface was coerced through the parsing path, which could invoke __toString() during construction and replace the original body instance.
  • Behavior change

    • Response now recognizes any PSR-7 StreamInterface as a direct body input.
    • When there is no request or raw-header parsing context, the constructor preserves the supplied stream instead of parsing it.
  • Coverage added

    • Added a regression test that constructs Response with a mocked StreamInterface and asserts:
      • the body instance is preserved
      • __toString() is not called during construction
$body = $this->getMockBuilder(StreamInterface::class)->getMock();
$body->expects(static::never())->method('__toString');

$response = new Response($body);

static::assertSame($body, $response->getBody());

This change is Reviewable

Copilot AI and others added 5 commits April 26, 2026 07:23
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>
@voku voku marked this pull request as ready for review April 26, 2026 17:51
@voku
Copy link
Copy Markdown
Owner

voku commented Apr 26, 2026

/gemini review

@coveralls
Copy link
Copy Markdown

coveralls commented Apr 26, 2026

Coverage Status

coverage: 89.871% (+0.4%) from 89.439% — copilot/increase-code-coverage-validation into master

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/Httpful/Response.php Outdated
$this->_interpretHeaders();

$bodyParsed = $this->_parse($body);
$bodyParsed = $this->shouldPreserveBodyStream($body) ? $body : $this->_parse($body);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Update the method call to match the suggested rename to _shouldPreserveBodyStream to maintain consistency with the repository's naming convention for private methods.

        $bodyParsed = $this->_shouldPreserveBodyStream($body) ? $body : $this->_parse($body);

Comment thread src/Httpful/Response.php Outdated
return Setup::setupGlobalMimeType($parse_with)->parse((string) $body);
}

private function shouldPreserveBodyStream($body): bool
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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): bool
References
  1. Private and internal methods in this repository follow a convention of being prefixed with an underscore. (link)

Comment thread src/Httpful/Response.php Outdated
Comment on lines +874 to +875
return $body instanceof StreamInterface
&& $this->request === null
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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 === null

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 StreamInterface as a first-class response body input in Response::__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.

Comment thread src/Httpful/Response.php Outdated

$bodyParsed = $this->_parse($body);
$bodyParsed = $this->shouldPreserveBodyStream($body) ? $body : $this->_parse($body);
$this->body = Stream::createNotNull($bodyParsed);
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
$this->body = Stream::createNotNull($bodyParsed);
$this->body = $bodyParsed instanceof StreamInterface
? $bodyParsed
: Stream::createNotNull($bodyParsed);

Copilot uses AI. Check for mistakes.
Comment on lines +86 to +94
public function testConstructorPreservesGenericStreamBodyWithoutParsingContext()
{
$body = $this->getMockBuilder(StreamInterface::class)->getMock();
$body->expects(static::never())
->method('__toString');

$r = new Response($body);
static::assertSame($body, $r->getBody());
}
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/Httpful/Response.php
Comment on lines 95 to 135
@@ -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;
}
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 uses AI. Check for mistakes.
@voku
Copy link
Copy Markdown
Owner

voku commented Apr 26, 2026

@copilot apply changes based on the comments in this thread PS: let's revert the code changes and rework it the TDD way

Copy link
Copy Markdown
Author

Copilot AI commented Apr 26, 2026

@copilot apply changes based on the comments in this thread PS: let's revert the code changes and rework it the TDD way

Addressed in 2278465. I reverted the earlier constructor change, reintroduced the fix TDD-first with a failing regression test, and reworked Response so only original StreamInterface inputs are preserved directly while scalar inputs stay on the old parse path. No UI changes in this library PR, so there isn’t a screenshot to attach.

Copilot AI requested a review from voku April 26, 2026 18:51
Copilot finished work on behalf of voku April 26, 2026 18:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants