skip to content

In Laravel's HTTP client, does Http::get() throw an exception when the server answers 404 or 500, and how do you make it throw?

level: juniorimportance: must knowfreq 62%

answer

  1. which Guzzle default Laravel changes
  2. http_errors false by default
  3. successful() vs failed()
  4. throw() returns the response
  5. RequestException has public ->response

basics

~10 s

No. Laravel's Http client returns a Response for 4xx and 5xx statuses; check successful() or failed(), or call throw() to raise RequestException. Timeouts and refused connections are different: they always throw ConnectionException.

solid answer

~30 s

Laravel creates every request with Guzzle's `http_errors` option set to `false`, so `Http::get()` hands back an `Illuminate\Http\Client\Response` whatever the status. You inspect it with `successful()` (2xx), `failed()` (4xx or 5xx), `clientError()` or `serverError()`. To turn an error status into an exception, call `$response->throw()` after the call, or `Http::throw()->get(...)` before it; both raise `Illuminate\Http\Client\RequestException`, whose public `$response` property holds the failed response and whose code is the status. `throw()` returns the response when nothing failed, so `->throw()->json()` chains cleanly. Transport failures are separate: a timeout or refused connection throws `ConnectionException` even without `throw()`. Both classes extend `HttpClientException`.

code

php · 19 lines
php
<?php

use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;

try {
    $rates = Http::acceptJson()
        ->get('https://carrier.example/api/rates', ['zip' => '10115'])
        ->throw()
        ->json('rates');
} catch (RequestException $e) {
    report($e);
    $status = $e->response->status(); // 404, 422, 503 ...
    $rates = [];
} catch (ConnectionException $e) {
    report($e); // timeout, DNS failure, refused connection
    $rates = [];
}

go deeper

for a junior

Remember that Laravel's HTTP client returns a Response for error statuses and only throws when you call throw(). Know successful() and failed().

for a middle

Explain http_errors being off, the throw()/throwIf() family, the public $response on RequestException, and why ConnectionException is a separate class.

for a senior

Show you design catch blocks around both failure classes, never read json() from an unchecked 500, and tune exception truncation so logs keep useful body detail.

for a principal

Frame the tradeoff: returning responses keeps 4xx business results cheap to handle, while a team rule to always throw() or wrap calls in a client class prevents silent failures.

## Why a 500 does not throw by default Laravel's HTTP client is a fluent layer over **Guzzle**, the PHP HTTP library. Every request starts life as an `Illuminate\Http\Client\PendingRequest`, and its constructor sets a few Guzzle options: a `timeout` of 30 seconds, a `connect_timeout` of 10 seconds and **`http_errors` set to `false`**. That last option is why `Http::get()` never throws because of a status code. Whatever the server answers — 200, 404, 422 or 503 — you get back an `Illuminate\Http\Client\Response`, and deciding what counts as failure is your job. The choice is deliberate. Many APIs use 4xx answers as ordinary business results — a 404 for "no such tracking number", a 422 for "address rejected" — and a returned response lets you branch on them without `try`/`catch`. ## Checking the status yourself The `Response` class carries a predicate for each common case: | Method | True when | |---|---| | `successful()` | the status is 200-299 | | `ok()` | the status is exactly 200 | | `failed()` | the status is 400 or above | | `clientError()` | the status is 400-499 | | `serverError()` | the status is 500 or above | | `onError($callback)` | runs the callback at once if `failed()` is true | Named helpers such as `notFound()`, `unauthorized()` and `tooManyRequests()` exist too, and `status()` returns the raw code. A 3xx answer that was not followed is neither `successful()` nor `failed()`. ## Reading data from the response The same `Response` object gives you the body in several shapes, and every one of them works on an error response too — which is exactly why an unchecked error is dangerous: - `body()` returns the raw string; - `json()` decodes it to arrays, and `json('rates.0.price')` reads one key with dot notation, returning `null` when the key is missing; - `object()` decodes to `stdClass` objects, `collect()` wraps the decoded data in a Laravel collection, and `fluent()` wraps it in a `Fluent` object; - array access works directly: `$response['rates']` reads the decoded JSON; - `header('Retry-After')` and `headers()` expose the response headers. If the carrier returns `{"error": "account suspended"}` with a 403, `json('rates')` is simply `null`, and code downstream treats "no rates" as a normal answer. Checking the status first — or throwing — is what separates "the carrier has no rates for this address" from "the carrier refused us". ## Turning an error status into an exception When a failed status should stop the flow, ask for an exception explicitly. There are two places to do it: 1. **After the response arrives**: `$response->throw()` throws `Illuminate\Http\Client\RequestException` if `failed()` is true, and otherwise **returns the same response**, so `Http::get($url)->throw()->json()` reads naturally. 2. **Before sending**: `Http::throw()->get($url)` configures the pending request so the check runs automatically when the response arrives. Conditional variants exist: - `throwIf($condition)` and `throwUnless($condition)` take a boolean or a closure that receives the response, on both the pending request and the response. - `throwIfStatus(403)` and `throwUnlessStatus(200)` on a response target a single code. - `throwIfClientError()` and `throwIfServerError()` on a response target one class of error. Passing a closure to `throw()` runs it with the response and the exception just before the exception is thrown — handy for adding log context — and you do not re-throw it yourself. ## What the exception carries `RequestException` has a public **`$response`** property holding the failed `Response`, so a handler can read `$e->response->status()` or `$e->response->json('error')`. Its exception code is the HTTP status. Its message reads "HTTP request returned status code 503" followed by a summary of the body, **truncated to 120 characters by default**. Change that globally with `RequestException::truncateAt(240)` or `RequestException::dontTruncate()` (the documentation registers these in `bootstrap/app.php`), or per request with `Http::truncateExceptionsAt(240)`. ## Transport failures are a different class A status code needs a response. When there is none — the connection is refused, DNS lookup fails, or the `timeout` expires — Laravel throws **`Illuminate\Http\Client\ConnectionException`**, and it does so **whether or not you called `throw()`**. The hierarchy matters for catch blocks: - `RequestException` extends `HttpClientException`: a response arrived with a 4xx or 5xx status. - `ConnectionException` extends `HttpClientException`: no usable response arrived. - Catching `HttpClientException` covers both; catching only `RequestException` lets timeouts escape. ## Putting it together For a flaky shipping-carrier API, a typical call reads rates, turns error statuses into exceptions with `throw()`, and catches the two failure classes separately: a `RequestException` means the carrier answered with an error you can inspect, while a `ConnectionException` means no answer came back and a fallback is needed. Code that forgets `throw()` and immediately calls `$response->json('rates')` on a 500 quietly gets `null` instead of failing loudly — the most common bug this default causes.

  • What happens when you pass a closure to throw() in Laravel's HTTP client?
    The closure runs with the `Response` and the `RequestException` just before the exception is thrown, only when the response failed. It is for side effects such as logging or adding context; the exception is still thrown afterwards, so you do not re-throw it inside the closure.
  • Why might a logged RequestException show only part of the carrier's error body?
    Its message appends a body summary truncated to 120 characters by default. Raise or disable the limit globally with `RequestException::truncateAt(240)` or `RequestException::dontTruncate()`, or per request with `Http::truncateExceptionsAt(240)`. The full body is always available on `$e->response->body()`.
  • How do you catch both a failed status and a timeout from Laravel's HTTP client in one catch block?
    Catch `Illuminate\Http\Client\HttpClientException`, the parent of both `RequestException` and `ConnectionException`. Separate catch blocks are usually better, though: only `RequestException` carries a `$response` you can inspect.

saying these in an interview costs you the question

  • Http::get() throws automatically on any 404 or 500 response.
  • throw() returns true or null, so you cannot chain json() after it.
  • A timeout comes back as a Response with a 504 status you check with failed().
  • Catching RequestException also catches timeouts and DNS failures.
  • The failed response is lost once RequestException is thrown.