Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions src/Barstool.php
Original file line number Diff line number Diff line change
Expand Up @@ -265,15 +265,22 @@ public static function getResponseBody(Response $response): string
return 'REDACTED';
}

$body = $response->body();
// Non-seekable bodies (e.g. Guzzle's `stream => true`) can only be read once, so reading
// one here would leave it at EOF and hand the application an empty body. Record a
// placeholder instead, matching how streamed request bodies are handled.
if (! $response->getPsrResponse()->getBody()->isSeekable()) {
return '<Streamed Body>';
}

$contentTypeHeaderKey = $response->headers()->get('Content-Type') ? 'Content-Type' : 'content-type';

if (Str::startsWith(mb_strtolower((string) $response->headers()->get($contentTypeHeaderKey)), self::supportedContentTypes())) {
return self::checkContentSize($body) ? $body : '<Unsupported Barstool Response Content>';
if (! Str::startsWith(mb_strtolower((string) $response->headers()->get($contentTypeHeaderKey)), self::supportedContentTypes())) {
return '<Unsupported Barstool Response Content>';
}

return '<Unsupported Barstool Response Content>';
$body = $response->body();

return self::checkContentSize($body) ? $body : '<Unsupported Barstool Response Content>';
}

/**
Expand Down
37 changes: 37 additions & 0 deletions tests/BarstoolTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

declare(strict_types=1);

use GuzzleHttp\Psr7\Utils;
use GuzzleHttp\Psr7\NoSeekStream;
use Saloon\Http\Faking\MockClient;
use Saloon\Barstool\Models\Barstool;
use Saloon\Http\Faking\MockResponse;
Expand All @@ -10,11 +12,16 @@
use Saloon\Barstool\Enums\RecordingType;
use Saloon\Http\Connectors\NullConnector;
use Saloon\Barstool\Jobs\RecordBarstoolJob;
use Saloon\Http\Response as SaloonResponse;

use function Pest\Laravel\assertDatabaseHas;

use GuzzleHttp\Psr7\Response as Psr7Response;

use function Pest\Laravel\assertDatabaseCount;
use function Pest\Laravel\assertDatabaseEmpty;

use Saloon\Barstool\Barstool as BarstoolRecorder;
use Saloon\Exceptions\Request\FatalRequestException;
use Saloon\Barstool\Tests\Fixtures\Requests\PostRequest;
use Saloon\Barstool\Tests\Fixtures\Requests\GetFileRequest;
Expand Down Expand Up @@ -714,6 +721,36 @@
});
});

it('does not consume non-seekable streamed response bodies', function () {
config()->set('barstool.enabled', true);

// Sending with Guzzle's `stream => true` config produces a response backed by a
// non-seekable socket stream, which can only be read once. We recreate that
// here with a NoSeekStream so the test doesn't need a real HTTP call.

$pendingRequest = (new SoloUserRequest)->createPendingRequest();

$psrResponse = new Psr7Response(
status: 200,
headers: ['Content-Type' => 'application/json'],
body: new NoSeekStream(Utils::streamFor('{"data":"yeehaw"}')),
);

$response = SaloonResponse::fromPsrResponse($psrResponse, $pendingRequest, $pendingRequest->createPsrRequest());

BarstoolRecorder::record($response);

$barstool = Barstool::where('uuid', $pendingRequest->headers()->get('X-Barstool-UUID'))->sole();

expect($barstool)
->response_status->toBe(200)
->successful->toBeTrue()
->response_body->toBe('<Streamed Body>');

// Recording must not have drained the stream - the application still needs to read it
expect($response->stream()->getContents())->toBe('{"data":"yeehaw"}');
});

it('does not dispatch jobs when queue is disabled', function () {
Queue::fake();

Expand Down