8
8
use ImageKit \Core \Contracts \BaseStream ;
9
9
use ImageKit \Core \Conversion \Contracts \Converter ;
10
10
use ImageKit \Core \Conversion \Contracts \ConverterSource ;
11
+ use ImageKit \Core \Exceptions \APIConnectionException ;
11
12
use ImageKit \Core \Exceptions \APIStatusException ;
12
13
use ImageKit \RequestOptions ;
14
+ use Psr \Http \Client \ClientExceptionInterface ;
13
15
use Psr \Http \Client \ClientInterface ;
14
16
use Psr \Http \Message \RequestFactoryInterface ;
15
17
use Psr \Http \Message \RequestInterface ;
@@ -41,7 +43,7 @@ public function __construct(
41
43
string $ baseUrl ,
42
44
protected RequestOptions $ options = new RequestOptions ,
43
45
) {
44
- assert (null !== $ this ->options ->uriFactory );
46
+ assert (! is_null ( $ this ->options ->uriFactory ) );
45
47
$ this ->baseUrl = $ this ->options ->uriFactory ->createUri ($ baseUrl );
46
48
}
47
49
@@ -67,7 +69,7 @@ public function request(
67
69
// @phpstan-ignore-next-line
68
70
[$ req , $ opts ] = $ this ->buildRequest (method: $ method , path: $ path , query: $ query , headers: $ headers , body: $ body , opts: $ options );
69
71
['method ' => $ method , 'path ' => $ uri , 'headers ' => $ headers ] = $ req ;
70
- assert (null !== $ opts ->requestFactory );
72
+ assert (! is_null ( $ opts ->requestFactory ) );
71
73
72
74
$ request = $ opts ->requestFactory ->createRequest ($ method , uri: $ uri );
73
75
$ request = Util::withSetHeaders ($ request , headers: $ headers );
@@ -170,14 +172,63 @@ protected function followRedirect(
170
172
): RequestInterface {
171
173
$ location = $ rsp ->getHeaderLine ('Location ' );
172
174
if (!$ location ) {
173
- throw new \ RuntimeException ( 'Redirection without Location header ' );
175
+ throw new APIConnectionException ( $ req , message: 'Redirection without Location header ' );
174
176
}
175
177
176
178
$ uri = Util::joinUri ($ req ->getUri (), path: $ location );
177
179
178
180
return $ req ->withUri ($ uri );
179
181
}
180
182
183
+ /**
184
+ * @internal
185
+ */
186
+ protected function shouldRetry (
187
+ RequestOptions $ opts ,
188
+ int $ retryCount ,
189
+ ?ResponseInterface $ rsp
190
+ ): bool {
191
+ if ($ retryCount >= $ opts ->maxRetries ) {
192
+ return false ;
193
+ }
194
+
195
+ $ code = $ rsp ?->getStatusCode();
196
+ if (408 == $ code || 409 == $ code || 429 == $ code || $ code >= 500 ) {
197
+ return true ;
198
+ }
199
+
200
+ return false ;
201
+ }
202
+
203
+ /**
204
+ * @internal
205
+ */
206
+ protected function retryDelay (
207
+ RequestOptions $ opts ,
208
+ int $ retryCount ,
209
+ ?ResponseInterface $ rsp
210
+ ): float {
211
+ if (!empty ($ header = $ rsp ?->getHeaderLine('retry-after ' ))) {
212
+ if (is_numeric ($ header )) {
213
+ return floatval ($ header );
214
+ }
215
+
216
+ try {
217
+ $ date = new \DateTimeImmutable ($ header );
218
+ $ span = time () - $ date ->getTimestamp ();
219
+
220
+ return max (0.0 , $ span );
221
+ } catch (\DateMalformedStringException ) {
222
+ }
223
+ }
224
+
225
+ $ scale = $ retryCount ** 2 ;
226
+ $ jitter = 1 - (0.25 * mt_rand () / mt_getrandmax ());
227
+ $ naive = $ opts ->initialRetryDelay * $ scale * $ jitter ;
228
+
229
+ return max (0.0 , min ($ naive , $ opts ->maxRetryDelay ));
230
+ }
231
+
181
232
/**
182
233
* @internal
183
234
*
@@ -194,25 +245,40 @@ protected function sendRequest(
194
245
assert (null !== $ opts ->streamFactory && null !== $ opts ->transporter );
195
246
196
247
$ req = Util::withSetBody ($ opts ->streamFactory , req: $ req , body: $ data );
197
- $ rsp = $ opts ->transporter ->sendRequest ($ req );
198
- $ code = $ rsp ->getStatusCode ();
248
+
249
+ $ rsp = null ;
250
+ $ err = null ;
251
+
252
+ try {
253
+ $ rsp = $ opts ->transporter ->sendRequest ($ req );
254
+ } catch (ClientExceptionInterface $ e ) {
255
+ $ err = $ e ;
256
+ }
257
+
258
+ $ code = $ rsp ?->getStatusCode();
199
259
200
260
if ($ code >= 300 && $ code < 400 ) {
261
+ assert (!is_null ($ rsp ));
262
+
201
263
if ($ redirectCount >= 20 ) {
202
- throw new \ RuntimeException ( 'Maximum redirects exceeded ' );
264
+ throw new APIConnectionException ( $ req , message: 'Maximum redirects exceeded ' );
203
265
}
204
266
205
267
$ req = $ this ->followRedirect ($ rsp , req: $ req );
206
268
207
269
return $ this ->sendRequest ($ opts , req: $ req , data: $ data , retryCount: $ retryCount , redirectCount: ++$ redirectCount );
208
270
}
209
271
210
- if ($ code >= 400 && $ code < 500 ) {
211
- throw APIStatusException::from (request: $ req , response: $ rsp );
212
- }
272
+ if ($ code >= 400 || is_null ($ rsp )) {
273
+ if ($ this ->shouldRetry ($ opts , retryCount: $ retryCount , rsp: $ rsp )) {
274
+ $ exn = is_null ($ rsp ) ? new APIConnectionException ($ req , previous: $ err ) : APIStatusException::from (request: $ req , response: $ rsp );
275
+
276
+ throw $ exn ;
277
+ }
213
278
214
- if ($ code >= 500 && $ retryCount < $ opts ->maxRetries ) {
215
- usleep ((int ) $ opts ->initialRetryDelay );
279
+ $ seconds = $ this ->retryDelay ($ opts , retryCount: $ redirectCount , rsp: $ rsp );
280
+ $ floor = floor ($ seconds );
281
+ time_nanosleep ((int ) $ floor , nanoseconds: (int ) ($ seconds - $ floor ) * 10 ** 9 );
216
282
217
283
return $ this ->sendRequest ($ opts , req: $ req , data: $ data , retryCount: ++$ retryCount , redirectCount: $ redirectCount );
218
284
}
0 commit comments