Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Http Server Laravel Package

amphp/http-server

A non-blocking, concurrent HTTP/1.1 and HTTP/2 application server for PHP built on Revolt and Amp. It forwards requests to RequestHandler implementations and supports middleware, TLS, GZIP compression, and fiber-based concurrency.

View on GitHub
Deep Wiki
Context7

Getting Started

Know what you're installing. amphp/http-server is not a Laravel HTTP kernel replacement you can drop into public/index.php. It's a long-running, single-process, event-loop-driven server (Revolt + fibers) that owns the socket itself. There is no artisan serve, no per-request bootstrap, no $_SERVER, no app()->handle(). If you go in expecting Laravel's router, you'll fight it for a day and give up.

Where it does fit a Laravel shop: sidecar services that need high concurrency and cheap idle connections — SSE/long-poll endpoints, webhook receivers, WebSocket gateways, internal API aggregators, and streaming proxies — running next to your Laravel app, not instead of it.

Minimum viable install:

composer require amphp/http-server
# optional, big perf win on HTTP/2 and a good idea on hot paths:
# install libnghttp2 and enable FFI

PHP 8.1+ required. Add amphp/http-server-router, -static-content, and -form-parser as needed; they're separate packages.

First file to write (this is the whole mental model in one blob):

<?php
use Amp\Http\HttpStatus;
use Amp\Http\Server\{DefaultErrorHandler, Request, RequestHandler, Response, SocketHttpServer};
use Amp\Log\{ConsoleFormatter, StreamHandler};
use Amp\ByteStream;
use Monolog\Logger;

require __DIR__ . '/vendor/autoload.php';

$logger = new Logger('sidecar');
$handler = new StreamHandler(ByteStream::getStdout());
$handler->setFormatter(new ConsoleFormatter());
$logger->pushHandler($handler);

$requestHandler = new class implements RequestHandler {
    public function handleRequest(Request $request): Response
    {
        return new Response(
            status: HttpStatus::OK,
            headers: ['content-type' => 'application/json'],
            body: json_encode(['ok' => true, 'path' => $request->getUri()->getPath()]),
        );
    }
};

$server = SocketHttpServer::createForDirectAccess($logger);
$server->expose('127.0.0.1:8081');
$server->start($requestHandler, new DefaultErrorHandler());

Amp\trapSignal([SIGINT, SIGTERM]);
$server->stop();

Read in this order:

  1. README sections RequestHandler, Middleware, Request, Response — the entire API surface is small.
  2. The Amp docs on Future, async(), and combinators (Amp\Future\await()), because that is where the leverage is.
  3. amphp/socket if you ever touch the constructor directly (TLS, custom binding).

First real use case to ship: a webhook receiver or an SSE status endpoint. Both are read-mostly, concurrency-bound, and can live behind nginx on a separate port without touching a single line of Laravel routing. Prove the model on something that can't take down your app.

Implementation Patterns

Keep Laravel as the source of truth; make the async service a thin edge

The pattern that survives contact with production: nginx routes /api/* to Laravel-FPM as usual, and routes /events, /ws, /hooks/* to the amphp process. The amphp service does not boot Eloquent. It gets data by:

  • amphp/http-client calling back into Laravel's internal API (cache the results with amphp/cache),
  • amphp/redis subscribing to a pub/sub channel Laravel publishes to,
  • amphp/postgres / amphp/mysql for read-only queries on a narrow schema.

Anything that smells like "let me just use the DB facade here" is the bug that kills the service at 3am.

Handler-per-endpoint plus the router package

Don't build a God handler. One RequestHandler class per endpoint, composed with amphp/http-server-router:

$router = new Amp\Http\Server\Router;
$router->addRoute('GET', '/health', new HealthHandler);
$router->addRoute('POST', '/hooks/github', new GithubWebhookHandler($queue));
$router->setFallback(new NotFoundHandler);

This keeps each handler trivially unit-testable — handleRequest() takes a Request and returns a Response, no container needed.

Middleware stack for the cross-cutting stuff

Middleware is the closest analogue to Laravel's middleware, and it composes the same way — first in the list is outermost:

use Amp\Http\Server\Middleware\stackMiddleware;

$stack = stackMiddleware(
    $router,
    new RequestIdMiddleware,       // outermost: sets header on the way out
    new BearerTokenMiddleware($tokenStore),
    new AccessLogMiddleware($logger),
    new CorsMiddleware(...),       // labrador-kennel/http-cors
);

Auth is a good example: verify a bearer token against Redis (amphp/redis) or a cached in-memory map, and short-circuit with a Response — never delegate to $next on failure. Response-time logging goes after $next->handleRequest($request) returns, and mutating the response there works fine because Response is mutable until sent.

Fan-out inside a request with Amp\async()

This is the payoff most Laravel devs never get: parallel downstream calls in one request without queues, curl_multi, or Octane hacks.

use function Amp\async;
use function Amp\Future\await;

$user    = async(fn() => $this->http->request(new Request('.../users/42', 'GET')));
$orders  = async(fn() => $this->http->request(new Request('.../orders?user=42', 'GET')));
$flags   = async(fn() => $this->flags->isEnabled('checkout-v2', 42));

[$userRes, $ordersRes, $flag] = await([$user, $orders, $flags]);

Wrap the whole thing in a timeout (see Gotchas) or one slow upstream parks the coroutine forever.

SSE / streaming responses

Return a Response whose body is a ReadableStream and push chunks as events arrive. Set content-type: text/event-stream, cache-control: no-store, and disable compression for that response. Always register a cleanup callback on the client so the stream producer stops when the browser tab closes:

$request->getClient()->onClose(fn() => $subscription->unsubscribe());

Offloading genuinely blocking work with amphp/parallel

When you must call Eloquent, an image resizer, or a legacy SDK: push it to a worker pool instead of blocking the loop.

use Amp\Parallel\Worker;
use function Amp\Parallel\Worker\submit;

$result = submit(new LegacyJob($payload))->await();

Treat this as the escape hatch, not the default. Every worker submission is a serialization boundary and a process you have to manage.

Operations

  • Run under systemd/Supervisor with a restart policy; trapSignal() then $server->stop() gives you graceful drain.
  • Config is read once at boot. env() is not re-evaluated per request — that's a feature (no .env parsing per request) and a footgun (change a value, restart the process).
  • Size the limits deliberately: createForDirectAccess() defaults to 10 connections/IP, 1000 total connections, 1000 concurrent requests. For an internal fan-out service those IP limits are wrong — build with the constructor or use createForBehindProxy().
  • Give each service its own PSR-3 logger writing to stdout (12-factor), and let the supervisor/journal collect it. Don't write files from the request path.

Gotchas and Tips

One blocking call stalls the whole world. sleep(), file_get_contents(), curl_exec(), mysqli_query(), Redis::get() via the C extension, Mail::send(), Storage::put() on a slow disk — all block the single thread and every concurrent request waits. Use Amp\delay(), amphp/file, amphp/http-client, amphp/redis. Audit every dependency before adding it; a "harmless" library that does file_get_contents on a config file can tank you.

Laravel facades and Eloquent do not belong in this process. Bootstrapping the container is blocking and slow, PDO is blocking, and the container's lifecycle assumptions (singleton Auth, request-bound state) are wrong for a process serving thousands of interleaved requests. If you need an Eloquent model's logic, extract it into a plain class with no container dependencies, or run it in a amphp/parallel worker.

Request bodies are one-shot streams. $request->getBody()->buffer() consumes the stream — a second read yields nothing. Same for amphp/http-server-form-parser: parse once, cache the result on your own object if multiple layers need it. Also cap the body size yourself before buffering; buffer() on an unbounded upload is a memory bomb.

ErrorHandler is not an exception handler. DefaultErrorHandler produces responses for malformed requests (bad headers, oversized, unsupported method). An exception thrown inside your RequestHandler does not route there — it tears down the connection. Wrap your handler bodies in try/catch (\Throwable $e), log with the request ID, and return a 500 yourself.

Pick the right constructor or client IPs will be wrong. createForDirectAccess() trusts nothing — put it behind nothing but a TCP load balancer that preserves the source. createForBehindProxy() requires an explicit ForwardedHeaderType and a trusted-proxy list; get that list wrong and X-Forwarded-For becomes a spoofable header your rate limiter believes. Mismatched choice is the #1 production incident with this package.

Compression is on by default. Good for JSON, wrong for SSE, wrong for already-gzipped payloads, and pointless for tiny responses. Turn it off per-server or per-response rather than debugging "why does my event stream arrive in 8KB bursts."

HTTP/2 in practice means TLS. Plaintext HTTP/2 (h2c) is rarely what you want to negotiate. Configure TLS via BindContext::withTlsContext() and pass it as the second argument to expose(). If you're terminating TLS at nginx — which you probably are — you're on HTTP/1.1 between nginx and the service, and that's fine.

No max_execution_time. PHP's timeout doesn't apply to event-loop coroutines, and a hung downstream hangs forever. Apply Amp\TimeoutCancellation (or Amp\Timeout\timeout()) around every external call, and put an overall deadline around multi-step handlers.

Middleware ordering is outside-in. The first middleware you pass to stackMiddleware() is the outermost; code after $next->handleRequest() runs in reverse order. This matters for CORS (must be outermost so error responses get headers too) and request IDs.

Header mutation semantics. setHeader() replaces, addHeader() appends. Multiple Set-Cookie requires addHeader(). Header names are case-insensitive on the wire but the array keys are as you set them — normalize if you're comparing.

Method allow-listing is on by default. createForDirectAccess() and createForBehindProxy() restrict request methods to a known set. Custom verbs (PROPFIND, vendor verbs) get rejected before reaching your handler. If you need them, build with the constructor and configure it.

Memory in a long-running process is your problem. Static caches, in-memory maps, and closed-but-referenced request objects accumulate forever. Keep caches bounded (TTL + max entries via amphp/cache), avoid storing Request/Response/Client references in statics, and check RSS over a soak test — not after a week in prod.

Don't debug with var_dump or Xdebug. Stray output goes to stdout and pollutes logs; Xdebug's step-breakpoints are blocking I/O and will freeze the loop, making concurrency bugs invisible. Log with request IDs and use Revolt's error handler for uncaught loop exceptions.

Watch for TCP_NODELAY. Nagle's algorithm adds latency to small responses on some systems; pass Amp\Socket\BindContext::withTcpNoDelay() as the second argument to expose() if benchmarks look oddly bad.

Extension points worth knowing: implement RequestHandler for endpoints, Middleware for cross-cutting concerns, a custom ErrorHandler for branded error pages, Client's onClose() for cleanup, and the raw SocketHttpServer constructor when you need custom SocketServerFactory / ClientFactory (connection limiting, TLS variants, Unix sockets). CallableRequestHandler is a fine escape valve for prototypes and tests — just don't ship anonymous classes as production route handlers.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
thomas-0816/doctrine-dbal-duckdb
moyisoft/laravel-admin
oxygen/mod-pages
oxygen/mod-import-export
oxygen/mod-media
ksoft/laravel-contentbuilderjs
ourgoldpackages/exchange
mikkimike/laravel-exchange1c
wandesnet/mercadopago-laravel
tsfcorp/email
larafony/core
laravel-enso/currencies
tartan/laravel-online-payment
wedevbr/celcoin-laravel
surazdott/nepali-date
devsquad-cockpit/laravel
coding-socks/laravel-upload-handler
artisanbarista/laravel-shield
instance-code/repository
flooris/prestashop-models