Skip to content

Commit 9d2b1c6

Browse files
authored
Merge pull request #440 from craigpotter/feature/fatal-exception-middleware
Feature | Fatal Exception Middleware
2 parents f33db6e + 79818bc commit 9d2b1c6

File tree

5 files changed

+497
-259
lines changed

5 files changed

+497
-259
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"friendsofphp/php-cs-fixer": "^3.5",
3232
"illuminate/collections": "^9.39 || ^10.0",
3333
"league/flysystem": "^3.0",
34-
"pestphp/pest": "^2.6",
34+
"pestphp/pest": "^2.10",
3535
"phpstan/phpstan": "^1.11.4",
3636
"saloonphp/xml-wrangler": "^1.1",
3737
"spatie/ray": "^1.33",

src/Helpers/MiddlewarePipeline.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Saloon\Enums\PipeOrder;
99
use Saloon\Http\PendingRequest;
1010
use Saloon\Contracts\FakeResponse;
11+
use Saloon\Exceptions\Request\FatalRequestException;
1112

1213
class MiddlewarePipeline
1314
{
@@ -21,13 +22,19 @@ class MiddlewarePipeline
2122
*/
2223
protected Pipeline $responsePipeline;
2324

25+
/**
26+
* Fatal Pipeline
27+
*/
28+
protected Pipeline $fatalPipeline;
29+
2430
/**
2531
* Constructor
2632
*/
2733
public function __construct()
2834
{
2935
$this->requestPipeline = new Pipeline;
3036
$this->responsePipeline = new Pipeline;
37+
$this->fatalPipeline = new Pipeline;
3138
}
3239

3340
/**
@@ -92,6 +99,33 @@ public function onResponse(callable $callable, ?string $name = null, ?PipeOrder
9299
return $this;
93100
}
94101

102+
/**
103+
* Add a middleware to run on fatal errors
104+
*
105+
* @param callable(FatalRequestException): (void) $callable
106+
* @return $this
107+
*/
108+
public function onFatalException(callable $callable, ?string $name = null, ?PipeOrder $order = null): static
109+
{
110+
/**
111+
* For some reason, PHP is not destructing non-static Closures, or 'things' using non-static Closures, correctly, keeping unused objects intact.
112+
* Using a *static* Closure, or re-binding it to an empty, anonymous class/object is a workaround for the issue.
113+
* If we don't, things using the MiddlewarePipeline, in turn, won't destruct.
114+
* Concretely speaking, for Saloon, this means that the Connector will *not* get destructed, and thereby also not the underlying client.
115+
* Which in turn leaves open file handles until the process terminates.
116+
*
117+
* Do note that this is entirely about our *wrapping* Closure below.
118+
* The provided callable doesn't affect the MiddlewarePipeline.
119+
*/
120+
$this->fatalPipeline->pipe(static function (FatalRequestException $throwable) use ($callable): FatalRequestException {
121+
$callable($throwable);
122+
123+
return $throwable;
124+
}, $name, $order);
125+
126+
return $this;
127+
}
128+
95129
/**
96130
* Process the request pipeline.
97131
*/
@@ -108,6 +142,15 @@ public function executeResponsePipeline(Response $response): Response
108142
return $this->responsePipeline->process($response);
109143
}
110144

145+
/**
146+
* Process the fatal pipeline.
147+
* @throws \Saloon\Exceptions\Request\FatalRequestException
148+
*/
149+
public function executeFatalPipeline(FatalRequestException $throwable): void
150+
{
151+
$this->fatalPipeline->process($throwable);
152+
}
153+
111154
/**
112155
* Merge in another middleware pipeline.
113156
*
@@ -125,8 +168,14 @@ public function merge(MiddlewarePipeline $middlewarePipeline): static
125168
$middlewarePipeline->getResponsePipeline()->getPipes()
126169
);
127170

171+
$fatalPipes = array_merge(
172+
$this->getFatalPipeline()->getPipes(),
173+
$middlewarePipeline->getFatalPipeline()->getPipes()
174+
);
175+
128176
$this->requestPipeline->setPipes($requestPipes);
129177
$this->responsePipeline->setPipes($responsePipes);
178+
$this->fatalPipeline->setPipes($fatalPipes);
130179

131180
return $this;
132181
}
@@ -146,4 +195,12 @@ public function getResponsePipeline(): Pipeline
146195
{
147196
return $this->responsePipeline;
148197
}
198+
199+
/**
200+
* Get the fatal pipeline
201+
*/
202+
public function getFatalPipeline(): Pipeline
203+
{
204+
return $this->fatalPipeline;
205+
}
149206
}

src/Http/PendingRequest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Saloon\Http\Middleware\ValidateProperties;
2424
use Saloon\Http\Middleware\DetermineMockResponse;
2525
use Saloon\Exceptions\InvalidResponseClassException;
26+
use Saloon\Exceptions\Request\FatalRequestException;
2627
use Saloon\Traits\PendingRequest\ManagesPsrRequests;
2728
use Saloon\Http\PendingRequest\MergeRequestProperties;
2829
use Saloon\Http\PendingRequest\BootConnectorAndRequest;
@@ -152,6 +153,14 @@ public function executeResponsePipeline(Response $response): Response
152153
return $this->middleware()->executeResponsePipeline($response);
153154
}
154155

156+
/**
157+
* Execute the fatal pipeline.
158+
*/
159+
public function executeFatalPipeline(FatalRequestException $throwable): void
160+
{
161+
$this->middleware()->executeFatalPipeline($throwable);
162+
}
163+
155164
/**
156165
* Get the request.
157166
*/

src/Traits/Connector/SendsRequests.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ public function send(Request $request, MockClient $mockClient = null, callable $
9292

9393
$exceptionResponse = $exception instanceof RequestException ? $exception->getResponse() : null;
9494

95+
// If the exception is a FatalRequestException, we'll execute the fatal pipeline
96+
if($exception instanceof FatalRequestException) {
97+
$exception->getPendingRequest()->executeFatalPipeline($exception);
98+
}
99+
95100
// If we've reached our max attempts - we won't try again, but we'll either
96101
// return the last response made or just throw an exception.
97102

0 commit comments

Comments
 (0)