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?
answer
- which Guzzle default Laravel changes
- http_errors false by default
- successful() vs failed()
- throw() returns the response
- RequestException has public ->response
basics
~10 sNo. 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 sLaravel 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
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
Remember that Laravel's HTTP client returns a Response for error statuses and only throws when you call throw(). Know successful() and failed().
Explain http_errors being off, the throw()/throwIf() family, the public $response on RequestException, and why ConnectionException is a separate class.
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.
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.