havvg/http-exception
havvg/http-exception provides lightweight, framework-agnostic HTTP exception classes for PHP, covering common 4xx and 5xx status codes with reason phrases. Throw typed exceptions to signal HTTP errors and map them cleanly to responses or middleware.
Install and inspect before you write code.
composer require havvg/http-exception
Then open vendor/havvg/http-exception/src/ and read two files first:
HttpException class — this is where the status code and headers live.HttpExceptionInterface — the contract (getStatusCode() / getHeaders()) you should catch against rather than catching concrete classes.The package is deliberately tiny: one exception class per HTTP status code (NotFoundException, BadRequestException, MethodNotAllowedException, etc.), each pre-seeded with its numeric status and a sensible default message. There is no service provider, no config file, and no framework coupling.
First use case — replace magic status codes in a controller:
use Havvg\HttpException\NotFoundException;
public function show(int $id): User
{
$user = User::find($id);
if (! $user) {
throw new NotFoundException("User {$id} does not exist.");
}
return $user;
}
Before adopting it, confirm two things: the php constraint in composer.json (this package is old — if it pins ~5.3 you will need a fork or --ignore-platform-reqs on PHP 8), and whether you intend to use it in Laravel at all. Laravel ships abort(404) and Symfony\Component\HttpKernel\Exception\HttpException, which cover the same ground. The value here is typed, self-documenting exceptions thrown from the domain layer rather than string-based abort() calls — not framework integration.
Throw low, render high. Let services and actions throw the concrete exception; let a single boundary (controller exception handler, middleware, or console kernel) convert it into a response. This keeps HTTP semantics out of your domain code's return types while still being explicit.
Catch the interface, not the class. Any generic handler should type-hint HttpExceptionInterface so new status codes you add later are handled automatically:
try {
$action->execute($request);
} catch (HttpExceptionInterface $e) {
return response($e->getMessage(), $e->getStatusCode(), $e->getHeaders());
}
Wire it into Laravel's render pipeline. In Laravel 11+, register renderable closures in bootstrap/app.php; on 10 and below, override render() in app/Exceptions/Handler.php:
->withExceptions(function (Exceptions $exceptions) {
$exceptions->render(function (HttpExceptionInterface $e) {
return response($e->getMessage(), $e->getStatusCode(), $e->getHeaders());
});
})
Extend for statuses the package is missing. Codes added to the RFC after the package's last release (429, 451, 422 with a specific body shape) are usually absent. Subclass the base and pin the code in the constructor:
class TooManyRequestsException extends HttpException
{
public function __construct(string $message = 'Too Many Requests', array $headers = [])
{
parent::__construct(429, $message, $headers);
}
}
Pass headers through the constructor. MethodNotAllowedException and UnauthorizedException are far more useful when you attach Allow: GET, POST or WWW-Authenticate: Bearer — carry them on the exception rather than reconstructing them at the render site.
Use the hierarchy for coarse fallbacks. If the package groups 4xx/5xx under intermediate base classes (as most of these collections do), you can write catch (ClientErrorException $e) to apply one policy — e.g. log at warning — and catch (HttpExceptionInterface $e) for server errors that should log at error with a stack trace.
Laravel will render these as 500s by default. The framework's handler special-cases Symfony\...\HttpExceptionInterface. An exception implementing only the Havvg interface falls through to the generic 500 path with a rendered error page. Always add the render binding above — this is the single most likely reason your 404 becomes a 500 in production.
Version and PHP compatibility is the real risk. With 0 dependents and low stars, treat this as unmaintained. Check the composer.json constraints before adding it to an existing project; if it blocks a PHP 8 upgrade, fork it into your own namespace rather than pinning your runtime. The classes are ~10 lines each, so vendoring is cheap.
Namespace and autoloading. Older releases in this family use PSR-0 with underscores in the namespace. If composer dump-autoload doesn't resolve the class, look at the autoload block and the directory layout together — don't assume src/ maps to Havvg\HttpException.
Don't let messages leak. These exceptions carry human-readable messages that are convenient in development and dangerous in production. Distinguish "safe to render" (validation, not-found) from "log only" (5xx), and strip messages for the 500 range.
Status codes are constructor arguments, not class constants. There's typically no NotFoundException::STATUS_CODE. If you need the number without instantiating, that's a sign you should be passing the exception around, not the integer.
Testing is trivial and worth doing. $this->expectException(NotFoundException::class) plus a status-code assertion on a caught instance is enough. If you build a render test, assert the response status and headers — header handling is the most common silent regression when you swap out the handler.
Consider whether you need it at all. For a Laravel app, abort(404) plus a small custom exception hierarchy in app/Exceptions/ gives you the same typed-exception ergonomics with zero dependency risk and full framework integration. Adopt this package when you want a ready-made, framework-agnostic set of classes — for a shared library, a Slim/Silex service, or a codebase that already speaks in these exception names.
How can I help you explore Laravel packages today?