Skip to content

Commit c8d7057

Browse files
committed
chore: add response body content limit
1 parent 0346119 commit c8d7057

File tree

9 files changed

+199
-28
lines changed

9 files changed

+199
-28
lines changed

config/saloon-utils.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
'enabled' => env('SALOON_REQUEST_LOGS', true),
88
// Pruning
99
'keep_for_days' => env('SALOON_REQUEST_PRUNE', 14),
10+
// The bundled migration uses longtext, which allows for 4,294,967,295 characters
11+
'response_max_length' => 4294967295,
1012
// Skip request logging
1113
'ignore' => [
1214
'connectors' => [],

src/Logger/LoggerRepository.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public function logResponse(Response $response, mixed $log, Connector $connector
7878
*/
7979
public function logFatalError(FatalRequestException $response, mixed $log, Connector $connector): mixed
8080
{
81-
// Initial request was not logged
81+
// The initial request was not logged
8282
if (empty($log)) {
8383
return null;
8484
}

src/Logger/SaloonRequest.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,30 @@
44

55
namespace HappyDemon\SaloonUtils\Logger;
66

7+
use Carbon\Carbon;
78
use Illuminate\Database\Eloquent\Builder;
89
use Illuminate\Database\Eloquent\MassPrunable;
910
use Illuminate\Database\Eloquent\Model;
1011

1112
/**
1213
* This model manages request logs
14+
*
15+
* @property int $id
16+
* @property string $connector The fully qualified class name of the connector
17+
* @property string $request The fully qualified class name of the request
18+
* @property string $method The HTTP method used
19+
* @property string $endpoint The endpoint that was called
20+
* @property array $request_headers The headers sent with the request
21+
* @property array $request_query The query parameters sent with the request
22+
* @property array $request_body The body sent with the request
23+
* @property array $response_headers The headers received in the response
24+
* @property array $response_body The body received in the response
25+
* @property int $status_code The HTTP status code received in the response
26+
* @property Carbon $completed_at When the request was completed
27+
* @property Carbon $created_at
28+
* @property Carbon $updated_at
29+
*
30+
* @mixin Builder
1331
*/
1432
class SaloonRequest extends Model
1533
{
@@ -58,7 +76,7 @@ protected function casts(): array
5876
*/
5977
public function prunable(): Builder
6078
{
61-
return static::where(
79+
return $this->newQuery()->where(
6280
'created_at',
6381
'<=',
6482
now()->startOfDay()->subDays(config('saloon-utils.logs.keep_for_days', 14))
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace HappyDemon\SaloonUtils\Logger\Stores;
6+
7+
use Saloon\Http\Response;
8+
9+
trait ConvertsResponseBody
10+
{
11+
public function convertResponseBody(Response $response): string
12+
{
13+
$contentType = $response->header('Content-Type') ?: $response->header('content-type');
14+
15+
if (in_array($contentType, [
16+
'application/json',
17+
'application/xml',
18+
'application/soap+xml',
19+
'text/xml',
20+
'text/html',
21+
'text/plain',
22+
null,
23+
], true)) {
24+
$body = $response->body();
25+
26+
return strlen($body) >= config('saloon-utils.logs.response_max_length') ? 'too large' : $body;
27+
}
28+
29+
return 'unsupported body: '.$contentType;
30+
}
31+
}

src/Logger/Stores/DatabaseLogger.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
class DatabaseLogger implements Logger
1515
{
16+
use ConvertsResponseBody;
17+
1618
public function create(PendingRequest $request, Connector $connector): mixed
1719
{
1820
$log = SaloonRequest::create([
@@ -35,7 +37,7 @@ public function updateWithResponse(mixed $log, Response $response, Connector $co
3537
{
3638
$log->update([
3739
'response_headers' => $response->headers()->all(),
38-
'response_body' => $response->body(),
40+
'response_body' => $this->convertResponseBody($response),
3941
'status_code' => $response->status(),
4042
'completed_at' => now(),
4143
]);

src/Logger/Stores/MemoryLogger.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
*/
2121
class MemoryLogger implements Logger
2222
{
23+
use ConvertsResponseBody;
24+
2325
protected Repository $store;
2426

2527
public function __construct()
@@ -69,7 +71,7 @@ public function updateWithResponse(mixed $log, Response $response, Connector $co
6971
{
7072
$log = array_merge($log, [
7173
'response_headers' => $response->headers()->all(),
72-
'response_body' => $response->body(),
74+
'response_body' => $this->convertResponseBody($response),
7375
'status_code' => $response->status(),
7476
'completed_at' => now(),
7577
]);

tests/Unit/Logger/DatabaseLoggerTest.php renamed to tests/Unit/Logger/Storage/DatabaseLoggerTest.php

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,52 @@
22

33
declare(strict_types=1);
44

5-
namespace HappyDemon\SaloonUtils\Tests\Unit\Logger;
5+
namespace HappyDemon\SaloonUtils\Tests\Unit\Logger\Storage;
66

77
use HappyDemon\SaloonUtils\Logger\SaloonRequest;
88
use HappyDemon\SaloonUtils\Tests\Saloon\Connectors\ConnectorFatal;
99
use HappyDemon\SaloonUtils\Tests\Saloon\Connectors\ConnectorGeneric;
1010
use HappyDemon\SaloonUtils\Tests\Saloon\Requests\GoogleSearchRequest;
1111
use HappyDemon\SaloonUtils\Tests\TestCaseDatabase;
12+
use PHPUnit\Framework\Attributes\DataProvider;
1213
use PHPUnit\Framework\Attributes\Test;
1314
use Saloon\Config;
1415
use Saloon\Exceptions\Request\FatalRequestException;
1516
use Saloon\Exceptions\Request\RequestException;
1617
use Saloon\Http\Faking\MockClient;
1718
use Saloon\Http\Faking\MockResponse;
1819

19-
class DatabaseLoggerTest extends TestCaseDatabase
20+
class DatabaseLoggerTest extends TestCaseDatabase implements StorageLoggerInterface
2021
{
2122
/**
2223
* @throws FatalRequestException
2324
* @throws RequestException
2425
*/
25-
#[Test]
26-
public function logs_response(): void
26+
protected function doSuccessfulRequest(?string $responseBody = null): void
2727
{
2828
$connector = app(ConnectorGeneric::class);
2929
$mockClient = new MockClient([
30-
GoogleSearchRequest::class => MockResponse::make('', 200),
30+
GoogleSearchRequest::class => MockResponse::make($responseBody ?: '', 200),
3131
]);
3232
$connector->withMockClient($mockClient);
3333

3434
// Send the request
3535
$connector->search('saloon');
36+
}
37+
38+
/**
39+
* @throws FatalRequestException
40+
* @throws RequestException
41+
*/
42+
#[Test]
43+
public function logs_response(): void
44+
{
45+
$this->doSuccessfulRequest();
3646

3747
$this->assertDatabaseCount((new SaloonRequest)->getTable(), 1);
3848

3949
/** @var SaloonRequest $log */
40-
$log = SaloonRequest::first();
50+
$log = SaloonRequest::query()->first();
4151
$request = new GoogleSearchRequest('saloon');
4252

4353
$this->assertEquals(200, $log->status_code);
@@ -53,7 +63,7 @@ public function logs_response(): void
5363
* @throws RequestException
5464
*/
5565
#[Test]
56-
public function logs_multiple(): void
66+
public function logs_multiple_responses(): void
5767
{
5868
$connector = app(ConnectorGeneric::class);
5969
$mockClient = new MockClient([
@@ -70,14 +80,31 @@ public function logs_multiple(): void
7080

7181
// verify the data matches
7282
$request = new GoogleSearchRequest($search);
73-
$log = $model->get()[$i];
7483

75-
$this->assertEquals(200, $log->status_code, 'status code should be 200');
76-
$this->assertEquals($request->query()->all(), $log->request_query, 'Query parameters should match');
77-
$this->assertEquals($request->resolveEndpoint(), $log->endpoint, 'Endpoint should have set correctly');
84+
/** @var SaloonRequest $log */
85+
$log = $model->newQuery()->get()[$i];
86+
87+
$this->assertEquals(
88+
200,
89+
$log->status_code,
90+
'status code should be 200'
91+
);
92+
$this->assertEquals(
93+
$request->query()->all(),
94+
$log->request_query,
95+
'Query parameters should match'
96+
);
97+
$this->assertEquals(
98+
$request->resolveEndpoint(),
99+
$log->endpoint,
100+
'Endpoint should have set correctly'
101+
);
78102
}
79103
}
80104

105+
/**
106+
* @throws RequestException
107+
*/
81108
#[Test]
82109
public function handles_fatal_error_correctly(): void
83110
{
@@ -92,12 +119,44 @@ public function handles_fatal_error_correctly(): void
92119
} catch (FatalRequestException $e) {
93120
$this->assertDatabaseCount((new SaloonRequest)->getTable(), 1);
94121

95-
$log = SaloonRequest::first();
122+
/** @var SaloonRequest $log */
123+
$log = SaloonRequest::query()->first();
96124
$request = new GoogleSearchRequest('saloon');
97125

98126
$this->assertEquals(418, $log->status_code);
99127
$this->assertEquals($request->query()->all(), $log->request_query);
100128
$this->assertEquals($request->resolveEndpoint(), $log->endpoint);
101129
}
102130
}
131+
132+
public static function bodySizes(): array
133+
{
134+
return [
135+
'exceeds limit' => [
136+
'sent' => '123456789101112',
137+
'stored' => 'too large',
138+
],
139+
'within limits' => [
140+
'sent' => 'data',
141+
'stored' => 'data',
142+
],
143+
];
144+
}
145+
146+
/**
147+
* @throws FatalRequestException
148+
* @throws RequestException
149+
*/
150+
#[Test]
151+
#[DataProvider('bodySizes')]
152+
public function response_body_size_is_respected(string $sent, string $stored): void
153+
{
154+
config()->set('saloon-utils.logs.response_max_length', 10);
155+
$this->doSuccessfulRequest($sent);
156+
157+
/** @var SaloonRequest $log */
158+
$log = SaloonRequest::query()->first();
159+
160+
$this->assertEquals($stored, $log->response_body);
161+
}
103162
}

tests/Unit/Logger/MemoryLoggerTest.php renamed to tests/Unit/Logger/Storage/MemoryLoggerTest.php

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@
22

33
declare(strict_types=1);
44

5-
namespace HappyDemon\SaloonUtils\Tests\Unit\Logger;
5+
namespace HappyDemon\SaloonUtils\Tests\Unit\Logger\Storage;
66

77
use HappyDemon\SaloonUtils\Logger\LoggerRepository;
88
use HappyDemon\SaloonUtils\Logger\Stores\MemoryLogger;
99
use HappyDemon\SaloonUtils\Tests\Saloon\Connectors\ConnectorFatal;
1010
use HappyDemon\SaloonUtils\Tests\Saloon\Connectors\ConnectorProvidesLogger;
1111
use HappyDemon\SaloonUtils\Tests\Saloon\Logger;
1212
use HappyDemon\SaloonUtils\Tests\Saloon\Requests\GoogleSearchRequest;
13-
use HappyDemon\SaloonUtils\Tests\TestCaseDatabase;
13+
use HappyDemon\SaloonUtils\Tests\TestCase;
1414
use Illuminate\Cache\Repository;
15+
use PHPUnit\Framework\Attributes\DataProvider;
1516
use PHPUnit\Framework\Attributes\Test;
1617
use Psr\SimpleCache\InvalidArgumentException;
1718
use ReflectionClass;
@@ -21,7 +22,7 @@
2122
use Saloon\Http\Faking\MockClient;
2223
use Saloon\Http\Faking\MockResponse;
2324

24-
class MemoryLoggerTest extends TestCaseDatabase
25+
class MemoryLoggerTest extends TestCase implements StorageLoggerInterface
2526
{
2627
protected function setUpFreshLoggerAndGetCache()
2728
{
@@ -34,6 +35,22 @@ protected function setUpFreshLoggerAndGetCache()
3435
return $storeProperty->getValue($logger);
3536
}
3637

38+
/**
39+
* @throws FatalRequestException
40+
* @throws RequestException
41+
*/
42+
protected function doSuccessfulRequest(?string $body = null): void
43+
{
44+
$connector = app(ConnectorProvidesLogger::class);
45+
$mockClient = new MockClient([
46+
GoogleSearchRequest::class => MockResponse::make($body ?: '', 200),
47+
]);
48+
$connector->withMockClient($mockClient);
49+
50+
// Send the request
51+
$connector->search('saloon');
52+
}
53+
3754
/**
3855
* @throws \ReflectionException
3956
*/
@@ -67,14 +84,7 @@ public function logger_inits_correctly(): void
6784
#[Test]
6885
public function logs_response(): void
6986
{
70-
$connector = app(ConnectorProvidesLogger::class);
71-
$mockClient = new MockClient([
72-
GoogleSearchRequest::class => MockResponse::make('', 200),
73-
]);
74-
$connector->withMockClient($mockClient);
75-
76-
// Send the request
77-
$connector->search('saloon');
87+
$this->doSuccessfulRequest();
7888

7989
$logger = app(MemoryLogger::class);
8090
$this->assertCount(1, $logger->logs());
@@ -93,7 +103,7 @@ public function logs_response(): void
93103
* @throws InvalidArgumentException
94104
*/
95105
#[Test]
96-
public function logs_multiple(): void
106+
public function logs_multiple_responses(): void
97107
{
98108
$connector = app(ConnectorProvidesLogger::class);
99109
$mockClient = new MockClient([
@@ -143,4 +153,35 @@ public function handles_fatal_error_correctly(): void
143153
$this->assertIsArray($log);
144154
}
145155
}
156+
157+
public static function bodySizes(): array
158+
{
159+
return [
160+
'exceeds limit' => [
161+
'sent' => '123456789101112',
162+
'stored' => 'too large',
163+
],
164+
'within limits' => [
165+
'sent' => 'data',
166+
'stored' => 'data',
167+
],
168+
];
169+
}
170+
171+
/**
172+
* @throws FatalRequestException
173+
* @throws RequestException
174+
* @throws InvalidArgumentException
175+
*/
176+
#[Test]
177+
#[DataProvider('bodySizes')]
178+
public function response_body_size_is_respected(string $sent, string $stored): void
179+
{
180+
config()->set('saloon-utils.logs.response_max_length', 10);
181+
$this->doSuccessfulRequest($sent);
182+
183+
$logs = (new MemoryLogger)->logs();
184+
185+
$this->assertEquals($stored, $logs[0]['response_body']);
186+
}
146187
}

0 commit comments

Comments
 (0)