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

innmind/http-server

A small HTTP server for PHP built on Innmind's HTTP abstractions. It wires PSR-7 requests and PSR-15-style middleware into an immutable application, handles the request lifecycle, and emits responses, giving you a framework-free foundation for serving APIs and web apps.

View on GitHub
Deep Wiki
Context7

Getting Started

innmind/http-server is not a framework — it's the edge layer that connects a PSR-7 request producer to a handler and a response emitter. You bring your own router, container and middleware; the package gives you a small set of interfaces plus a Main bootstrap that ties them together.

1. Install and check the constraints

composer require innmind/http-server

Before writing anything, open vendor/innmind/http-server/composer.json. Innmind packages track the latest PHP release aggressively, and releases are versioned by date (the current one is dated 2026-08-16). Pin an exact version in composer.json rather than a caret range — with zero dependents there is no ecosystem pressure to keep BC, and a composer update can silently raise your minimum PHP.

2. Read four files, in this order

File Why
src/Main.php The entry point. Its of()/__invoke() signature tells you the whole composition: process → server → handler → sender.
src/Server.php The interface you'll implement to plug in a runtime (built-in SAPI, RoadRunner, FrankenPHP, Swoole).
src/RequestHandler.php A single method, __invoke(ServerRequest): Response. This is where 95% of your code lives.
src/ResponseSender.php How bytes reach the client. Swap this to add gzip, streaming, or test doubles.

The tests/ directory is your real documentation — Innmind ships heavy test suites, and the fixtures show the intended wiring better than the README does. Also skim innmind/http (the PSR-7 implementation you'll be constructing) and innmind/immutable (Maybe, Either, Attempt) since handlers return those constantly.

3. First use case: a health endpoint

Start by getting one invokable handler to return a 200 with a JSON body, wired to the built-in server. That proves the loop end to end.

<?php
declare(strict_types=1);

namespace App\Http;

use Innmind\Http\Message\ServerRequest;
use Innmind\Http\Message\Response;
use Innmind\Http\Message\Response\Status;
use Innmind\Http\Message\Stream;
use Innmind\HttpServer\RequestHandler;

final class HealthCheck implements RequestHandler
{
    public function __invoke(ServerRequest $request): Response
    {
        return Response::of(
            Status::ok,
            $request->protocolVersion(),
            null,
            Stream::ofContent('{"status":"ok"}'),
        );
    }
}

Then compose it in your front controller:

// public/index.php
use Innmind\HttpServer\Main;

$main = Main::of(/* CurrentProcess / env */);

$main(
    $operatingSystem,
    $server,          // Innmind\HttpServer\Server
    $handler,         // your RequestHandler
    $responseSender,  // Innmind\HttpServer\ResponseSender
);

The exact argument list of Main::of() changes between releases — treat the method signature in src/Main.php as the source of truth and copy the shape from the newest test case. Once the health check responds, you have a working harness for everything else.


Implementation Patterns

Handlers are invokable, composable objects

RequestHandler is not PSR-15: there is no handle() and no MiddlewareInterface. Composition is done by decorator classes that implement the same interface and wrap an inner handler. This is the single most important pattern in this package.

final class CatchErrors implements RequestHandler
{
    public function __construct(private RequestHandler $inner) {}

    public function __invoke(ServerRequest $request): Response
    {
        try {
            return ($this->inner)($request);
        } catch (\Throwable $e) {
            // log, then return a deliberately opaque 500
            return Response::of(Status::internalServerError, $request->protocolVersion());
        }
    }
}

Build your stack as a pipeline, outermost first:

CatchErrors
  └─ LogRequests
       └─ AddRequestId
            └─ Router
                 └─ Controller

Write the wiring once in a factory function so the order is visible and testable:

function handler(Container $c): RequestHandler
{
    return new CatchErrors(
        new LogRequests(
            new AddRequestId(
                new Router($c->get(RouteTable::class)),
                $c->get(RequestIdGenerator::class),
            ),
            $c->get(LoggerInterface::class),
        ),
    );
}

Routing: match on the decomposed URL

innmind/http gives you real value objects, so route on them rather than on raw strings.

final class Router implements RequestHandler
{
    public function __invoke(ServerRequest $request): Response
    {
        $path   = $request->url()->path()->toString();
        $method = $request->method()->toString();

        return match (true) {
            $method === 'GET'  && $path === '/health' => ($this->health)($request),
            $method === 'GET'  && $path === '/users'  => ($this->listUsers)($request),
            $method === 'POST' && $path === '/users'  => ($this->createUser)($request),
            default => Response::of(Status::notFound, $request->protocolVersion()),
        };
    }
}

For anything beyond a dozen routes, generate a trie or a regex table at boot rather than match(true) per request — but keep the RequestHandler boundary so you can swap it out.

Decode with Maybe/Either, don't throw

The Innmind style is to keep failures in the return type. Parse the body into a string once, then hand it to innmind/validation or your own decoder that returns Either<Error, Command>:

public function __invoke(ServerRequest $request): Response
{
    $payload = $request->body()->toString();

    return $this->decode($payload)          // Either<ValidationError, CreateUser>
        ->map(fn(CreateUser $cmd) => $this->useCase->execute($cmd))
        ->match(
            fn(Response $r) => $r,
            fn(ValidationError $e) => $this->problem(Status::badRequest, $e),
        );
}

This keeps every handler branch-free at the top level and makes the error responses uniform.

Response helpers

Every handler constructing Response::of(...) by hand gets noisy fast. Add a tiny factory in your app (not in the vendor tree):

final class Json
{
    public static function of(Status $status, array $data, ProtocolVersion $v): Response
    {
        return Response::of(
            $status,
            $v,
            Headers::of(new Header('content-type', 'application/json')),
            Stream::ofContent(json_encode($data, JSON_THROW_ON_ERROR)),
        );
    }
}

Testing without a socket

Because a handler is a pure ServerRequest -> Response function, you never need a running server for unit tests:

$response = (new Router(...))($serverRequest);

$this->assertSame('200', $response->status()->toString());
$this->assertSame('{"status":"ok"}', $response->body()->toString());

Build ServerRequest instances directly from innmind/http's request factory in your test bootstrap, and keep a fixture builder so tests read like aRequest('POST', '/users', '{"name":"x"}').

To test the Main wiring itself, implement the two tiny interfaces with fakes: a Server that invokes the handler once with a canned request, and a ResponseSender that records what it was given. That catches argument-order mistakes in Main::of() without spinning up a process.

Long-running runtimes: keep handlers stateless

If you plug into RoadRunner/Swoole/FrankenPHP, the PHP process outlives the request. Handlers must be constructed at boot and hold no per-request state. Anything request-scoped (auth user, request ID, logger with context) travels as a parameter or a fresh decorator instance, never as a mutable property.


Gotchas and Tips

PSR-7 immutability bites everyone once. withHeader(), withBody(), withStatus() all return new instances. Forgetting to reassign is the most common bug:

$response->withHeader('x-request-id', $id);   // silently discarded
$response = $response->withHeader('x-request-id', $id); // correct

Not PSR-15. You cannot drop in middlewares/* or Slim/Laminas middleware. If you need one, write a small adapter wrapping a PSR-15 RequestHandlerInterface and translating ServerRequest in/out. Budget for this if you're migrating an existing app.

Uncaught exceptions depend on your wiring. Nothing in the package guarantees a 500 for you. If you forget the outermost CatchErrors decorator, a TypeError in a controller will either kill the process or leak a stack trace to the client depending on the Server implementation. Make the catch-all decorator the first thing you write, not the last.

Read the request body exactly once. Streams in this ecosystem are single-pass readables. Convert to a string at the top of the handler and pass the string around; passing the stream into two layers will give you an empty second read.

Never echo or var_dump inside a handler. Output goes to the process stdout, bypassing the ResponseSender, and corrupts the response (or, under FastCGI, produces "headers already sent"). For debugging, add a decorator that serialises the request/response to error_log() or a file, and remove it once the issue is fixed.

Content-Length and bodyless statuses. 204, 304 and responses to HEAD must have no body. The emitter is not necessarily smart about this — construct these responses without a stream rather than relying on the sender to strip it.

Headers: withHeader replaces, withAddedHeader appends. Multi-value headers (set-cookie, vary) need the latter, or you'll silently drop all but the last Set-Cookie.

Your own Server implementation is the extension point for anything unusual. If you need graceful shutdown on SIGTERM, connection draining, or access to the underlying socket, implement Server and keep the handler untouched. Same for ResponseSender — it's where streaming/chunked responses or gzip belong.

Zero stars, zero dependents means zero Stack Overflow. There is no community to ask. The compensating factor is that Innmind codebases are unusually well-tested and strictly typed; the tests/ folder and the type signatures are the documentation. Run git log --oneline vendor/... after an upgrade to see what the release actually changed — release notes are minimal.

Static analysis pays for itself here. The interfaces are precise, so Psalm or PHPStan at a high level will catch the PSR-7 misuses above (discarded immutable results, wrong protocol-version argument order) that PHP's runtime won't. Add psalm/psalm as a dev dependency and run it in CI from day one.

Watch the protocol version argument. Nearly every Response::of() call needs $request->protocolVersion() as the second argument; it's easy to hardcode '1.1' in a helper and then get confusing behaviour when a client speaks HTTP/2 or HTTP/1.0. Always thread the request's version through.

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.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource