hyperf/http-server
Hyperf HTTP Server provides a high-performance, coroutine-based HTTP server for the Hyperf framework, built on Swoole or Swow. It offers PSR-7/PSR-15 middleware support, powerful routing, annotation-driven controllers, and fast request handling for scalable PHP microservices.
Set expectations first: hyperf/http-server is not a Laravel package. It is the HTTP server component of the Hyperf framework — a coroutine-native, long-running PHP runtime built on Swoole/Swow. You cannot composer require it into a Laravel app and get anything useful; if you are staying on Laravel, this package is only worth reading as a reference for how a persistent-worker HTTP kernel is structured. If you are evaluating a Laravel → Hyperf migration, this is the component that replaces the Illuminate\Foundation\Http\Kernel + Request/Response stack.
Version note (v3.2.0): This release raises the floor to PHP >= 8.2, upgrades Symfony components to ^6.0 || ^7.0, and PHPUnit to ^11.0. If you are starting fresh, you get 3.2; if you are upgrading from 3.1, read the Gotchas section first — three config files (logger.php, cache.php, dependencies.php for Swow) changed shape.
Minimal install path (Hyperf app):
composer create-project hyperf/hyperf-skeleton my-app # brings http-server + router + DI
cd my-app
php bin/hyperf.php start # listens on 0.0.0.0:9501
Standalone in an existing Hyperf project:
composer require hyperf/http-server hyperf/router
If you are on Swow: v3.2 requires an explicit dependency mapping (Swoole installs do not need it):
// config/autoload/dependencies.php
return [
\Hyperf\Contract\ResponseEmitterInterface::class => \Hyperf\Engine\ResponseEmitter::class,
];
Where to look first, in this order:
config/autoload/server.php — the equivalent of public/index.php + kernel bootstrap. Look for the servers array and the entry mapping a port to Hyperf\HttpServer\Server::class.config/routes.php — imperative route definitions (Router::get('/hello', ...)).app/Controller/ — annotation-driven controllers (#[Controller], #[GetMapping]), the closest thing to Laravel's controller ergonomics.config/autoload/middlewares.php and config/autoload/exceptions.php — per-server middleware stack and exception handling.First use case — a JSON endpoint with injected request/response:
<?php
namespace App\Controller;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Contract\ResponseInterface;
use Hyperf\Di\Annotation\Inject;
class UserController
{
#[Inject]
protected RequestInterface $request;
#[Inject]
protected ResponseInterface $response;
public function show(int $id)
{
$verbose = $this->request->input('verbose', false);
$user = $this->request->route('id'); // route params are attributes
return $this->response->json([
'id' => $id,
'verbose' => $verbose,
'user' => $user,
]);
}
}
// config/routes.php
Router::get('/users/{id:\d+}', 'App\Controller\UserController@show');
Two things that immediately feel different from Laravel: (a) the controller is resolved from the DI container per request, not per-instantiation-per-request-per-call like FPM, and (b) you must return a Psr\Http\Message\ResponseInterface — echo and die() do not produce a response.
Controller injection. Constructor injection works, but #[Inject] on properties is the idiomatic Hyperf style for RequestInterface/ResponseInterface, because those contracts are proxy-resolved to the current coroutine's request. For anything request-scoped, prefer injecting the interface and letting the container hand you the coroutine-local instance — never new Request() yourself.
Routing: two coexisting styles. Annotation routing (#[Controller(prefix: '/api')], #[GetMapping(path: 'users/{id}')]) for app code, and config/routes.php for glue/health checks/proxies. Annotation routes are cached; see Gotchas. Route middleware is attached with #[Middleware(FooMiddleware::class)] per controller or per method — the closest analogue to Laravel's ->middleware() on route groups.
Middleware is PSR-15, not Laravel's pipeline. Signatures are process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface. Register globally per server in config/autoload/middlewares.php:
return [
'http' => [ // keyed by the server name from server.php
App\Middleware\CorsMiddleware::class,
App\Middleware\AuthMiddleware::class, // order = execution order
],
];
Because the key is the server name, a second HTTP server on port 9502 can have an entirely different middleware stack. This is a genuinely nice pattern for separating public API from internal/admin traffic in one process.
Exception → response mapping. Mirror Laravel's Handler::render() with Hyperf's exception handlers:
class ValidationExceptionHandler extends ExceptionHandler
{
public function handle(Throwable $throwable, ResponseInterface $response)
{
$this->stopPropagation();
return $response->json(['message' => $throwable->getMessage()], 422);
}
public function isValid(Throwable $throwable): bool
{
return $throwable instanceof ValidationException;
}
}
Register in config/autoload/exceptions.php under the same server name. Note the explicit stopPropagation() — without it the next handler (and eventually the framework's HttpExceptionHandler) also runs.
Request-scoped state: use Context, not singletons. This is the single biggest mental shift from Laravel. Any singleton (service, repository, listener) lives for the life of the worker, not the request. If you need "current user" in a service, stash it in the coroutine context:
use Hyperf\Context\Context;
Context::set('user', $user); // in middleware
$user = Context::get('user'); // anywhere downstream, same coroutine
As of v3.2, Hyperf\Context\Context is Macroable, so you can wrap common patterns (Context::macro('user', ...)) instead of stringly-typed keys scattered across the codebase. Still: never store a RequestInterface (or anything holding one) on a singleton or in a static property — the next coroutine will read the previous request's data.
Responding with HTML / JSON contracts. v3.2 added Hyperf\Contract\Jsonable::toJson() and an Htmlable contract for HTTP responses. If you build a response-like value object, implement these instead of hand-serialising in the controller — the response emitter recognises them and skips your manual json_encode.
Async work. v3.2 added automatic registration of async queue consumer processes from configuration — no more hand-bolting every consumer into config/autoload/processes.php. If you previously registered consumers manually, prefer the config-driven path; it keeps worker counts next to the queue definition instead of scattered in a bootstrap file.
Customizing the server. Extending Hyperf\HttpServer\Server and overriding onRequest is the extension point for global concerns that don't fit middleware (e.g. injecting a request ID into logs before dispatch, or short-circuiting on a maintenance flag). Register the subclass in server.php in place of the default class. Swoole-level knobs (max_request, package_max_length, open_http2_protocol, SWOOLE_BASE vs SWOOLE_PROCESS) live in the same array's settings key.
Integration tips. Use hyperf/validation for rules, but call it explicitly ($this->validator->validate(...)) — there is no $request->validate(). File uploads come from $request->file('avatar') and land in runtime/ unless you configure upload_tmp_dir. For streaming/large downloads, return a Psr\Http\Message\ResponseInterface backed by a stream rather than buffering a string. For tests, Hyperf\Testing\Client gives you a Laravel-ish $client->get('/users/1')->assertStatus(200) without a real socket; v3.2's #[NonCoroutine] attribute lets you mark specific tests that must run outside a coroutine, which is handy for anything touching Swow/Swoole internals.
config/autoload/logger.php now uses default as a channel name plus a channels map, instead of default being the channel definition itself. The old array silently produces "no logger" behaviour; existing path/driver/days settings must move under channels.default.config/autoload/cache.php now has default pointing at a store name plus a stores map. Same failure mode — old config looks valid, caches just miss or throw on boot.Carbon::createFromTimestamp() no longer falls back to date_default_timezone_get(); diffInDays() no longer returns an absolute value by default. Any code relying on either will drift silently after upgrade.JobInterface::getQueueName() renamed to getPoolName(). Custom jobs overriding the old method compile-fail; grep before you composer update.fputcsv() / fgetcsv() now require an explicit escape (pass escape: '') before the deprecation turns fatal in your error collector. The framework did this internally in 3.2, but any application-level CSV export needs the same treatment.$_GET, $_POST, $_SERVER, $_FILES, header(), setcookie() are all inert or misleading. Use $request->getQueryParams(), $request->getParsedBody(), $request->getHeaders(), $response->withCookie(...). Legacy code lifted from FPM apps will silently return empty data.#[Singleton] service in request A is still there in request B, and worse, is visible to a concurrent request in a different coroutine. Treat every singleton as immutable after boot. Same applies to the new Context macro cache — macros themselves are fine, but the keys you set are coroutine-scoped.sleep(), raw PDO, file_get_contents() on a remote URL, and heavy curl without hooks stall the entire worker. Either enable Swoole runtime hooks (SWOOLE_HOOK_ALL, usually on by default in the skeleton) or use the coroutine-native clients (hyperf/guzzle, hyperf/db-connection, hyperf/redis, Hyperf\Coroutine\System::sleep()). This is the #1 thing that makes a migrated Laravel app mysteriously slow under load.scan_cacheable enabled in config/autoload/annotations.php, new annotations are invisible until you regenerate the scan cache as part of your deploy. Make that a build step, not a debugging surprise.server.php, not .env alone. max_request (default ~100k) recycles workers — which is why long-lived in-memory caches are eventually wiped. package_max_length must be raised for large uploads; otherwise requests fail at the Swoole layer before your middleware ever runs, producing confusing empty-request errors.$request->getClientIp() returns the socket peer (your load balancer). Read X-Forwarded-For explicitly, and only trust it when the immediate peer is a known proxy.deleted_at defaults to datetime rather than timestamp (check your migrations and any direct SQL), QueryException now carries the connection name (use it in your exception handlers to distinguish primary vs replica failures), and limit() throws on negative values rather than silently coercing.runtime/logs/hyperf.log; enable the stdout logger (Hyperf\Framework\Logger\StdoutLogger) for fast feedback during development. v3.2 added a timestamp prefix to stdout log output — useful when you were previously guessing which line belonged to which request. var_dump inside a coroutine is interleaved across concurrent requests — include a request ID (set in middleware via Context) or you will misread the output. Watch mode: php bin/hyperf.php server:watch.isValid() returns true but you forget stopPropagation(), the framework's default handler overwrites your JSON with a generic 500 — a very common "my handler doesn't work" report.laminas/laminas-mime is gone; Hyperf\Serializer\Contract\CacheableSupportsMethodInterface is gone; Hyperf\AsyncQueue\JobMessage backward-compatible deserialisation is gone (old serialised payloads in Redis will fail to hydrate after the upgrade — drain the queue or accept a replay window); and a long tail of deprecated aliases (ResumeExitCoordinatorListener, SocketIO Future flag, WebSocket HandeShakeException typo alias) has been dropped.hyperf/http-server is a read-only split of the hyperf/hyperf monorepo; the star count and release cadence belong to the split repo, not the ecosystem. Judge adoption by the monorepo and the docs at hyperf.wiki — issues and PRs are filed upstream, so searching this repository alone will look like an abandoned project.illuminate/* components assume a synchronous container and superglobal-backed Request; they will not drop in. Budget for rewriting HTTP-layer code (form requests, middleware, exception rendering, session, CSRF) rather than expecting adapters. The payoff is that a Hyperf worker handles orders of magnitude more requests per process — but only if you honor the coroutine rules above.How can I help you explore Laravel packages today?