Error handling for Laravel's HTTP facade
I've been using Laravel's HTTP facade extensively since it was introduced in Laravel 7 back in 2020.
The HTTP facade is a user-friendly wrapper around Guzzle that also makes it easy to write unit tests by calling Http::fake()
.
The only thing that I've had trouble with when using the HTTP facade is error handling.
Sometimes it throws an exception, sometimes it doesn't, and depending on if you use retry()
this behavior changes.
This is the approach I've settled on that allows me to properly handle errors when using the HTTP facade:
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
try {
$response = Http::acceptJson()
->connectTimeout(5)
->timeout(10)
->throw()
// ->retry(times: 2, sleepMilliseconds: 500)
->get('https://example.com');
} catch (ConnectionException $e) {
// handle the error (no response is available)
} catch (RequestException $e) {
// handle the error (details are in "$e->response")
}
// handle the response (status code >= 200 && <= 399)
The code above catches all errors the HTTP facade can throw in a consistent way.
Catching the ConnectionException
The HTTP facade always throws a ConnectionException
when it fails to connect.
Failures to connect are inevitable, for example when DNS has a hiccup or when the remote server is down for maintenance.
The connectTimeout()
method sets how many seconds the HTTP facade should attempt to connect to the remote server.
If this time is exceeded and a connection still hasn't been made then a ConnectionException
is thrown.
The timeout()
method sets the total allowed duration of the request (including the connection).
If this time is exceeded a ConnectionException
is also thrown.
Catching the RequestException
The RequestException
is:
- Not thrown by default when a response has a 4xx/5xx status code
-
Thrown if
throw()
is used, and the response has a 4xx/5xx status code -
Thrown if
retry()
is used, and the response has a 4xx/5xx status code -
Not thrown
retry(throw: false)
is used
The key point here is that if you don't use throw()
, and you add retry()
to existing code, you'll completely change how errors should be handled.
To prevent this footgun I recommend always using throw()
, that way exceptions are always thrown regardless of if you use retry()
or not.
The retry method also has a parameter that disables throwing: retry(throw: false)
.
I can never seem to remember that this parameter exists, so I prefer always using throw()
instead.
HTTP requests and queued job timeouts
HTTP requests are usually made inside queued jobs.
It is important to make sure the queued job doesn't time out, because then your error handling code won't get called (unless you also have that logic in the failed()
method of your job).
To prevent queued jobs timing out, make sure that the sum of (HTTP timeout + sleep between retries) * amount of retries
is not higher than your job timeout.
To keep things simple I generally prefer retrying my queued jobs and not using the retry()
method on my HTTP requests, but this is personal preference.
Manually testing errors and timeouts
You can manually test how your code handles errors by calling these URLs:
-
Call
10.0.0.0
for a connection timeout -
Call
https://httpstat.us/200?sleep=15000
for a read timeout -
Call
https://httpstat.us/500
for a specific status code