Search by

bronsted / psr-http

sorenbronsted

Psr-http

Package info

github.com/sorenbronsted/psr-http

pkg:composer/bronsted/psr-http

Statistics

Installs: 74

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v3.0.3 2026-10-25 13:40 UTC

This package is auto-updated.

Last update: 2026-10-25 13:43:32 UTC


README

This is a http server and client package with PSR interfaces, so it can be used with frameworks which uses PSR interface. It is build with FiberLoop which is coroutines build on php fibers.

Version 3 requires Fiberloop 3.1, whose event loop is built around read and write watchers. HttpServer::getFiberLoop() returns that loop, so timers and deferred work can be scheduled on the same loop.

Install

You can install this package via Composer:

composer install bronsted\psr-http

Usage

Server usage:

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

use bronsted\HttpServer;
use Nyholm\Psr7\Factory\Psr17Factory;
use Psr\Http\Message\ServerRequestInterface;

$factory = new Psr17Factory();
$server = new HttpServer('0.0.0.0', 8000, $factory, $factory, $factory, $factory, $factory);

$server->run(function (ServerRequestInterface $request) use ($factory) {
    $response = $factory->createResponse();
    $path = $request->getUri()->getPath();
    if ($path == '/') {
        $params = $request->getQueryParams();
        $response->getBody()->write('Hello ' . (empty($params) ? 'world' : $params['name']));
    } else {
        $response = $response->withStatus(404);
    }
    return $response;
});

Use $server->getFiberLoop() to access the same event loop used by the server. Timers and deferred background work can be scheduled on it before calling run().

Standalone client usage context:

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

use bronsted\HttpClient;
use bronsted\FiberLoop;
use bronsted\StreamSocketFactory;
use Nyholm\Psr7\Factory\Psr17Factory;

FiberLoop::instance()->defer(function() use($argv) {
    $factory = new Psr17Factory();
    $request = $factory->createRequest('GET', $argv[1]);
    $client = new HttpClient($factory, new StreamSocketFactory());
    $response = $client->sendRequest($request);
    echo $response->getStatusCode() . PHP_EOL;
    echo $response->getBody()->getContents() . PHP_EOL;
});

FiberLoop::instance()->run();

HTTP behavior and limits

The v2.0 logger and timeout constructor arguments remain supported. HttpClient accepts an optional PSR-3 logger as its third argument and a connection timeout in seconds as its fourth argument (default 5). HttpServer accepts an optional PSR-3 logger after its seven factory/address arguments, before the optional size limits. Omitting the logger uses a NullLogger. Request timing and failures are logged without allowing a logging backend failure to interrupt HTTP processing. The standalone static Log helper remains available for compatibility.

The server handles one request per connection and sends Connection: close. Exceptions from a request handler produce a 500 response; a disconnected client or failed response write does not stop the server's event loop.

Request lines are limited to 8,000 bytes, individual header lines to 8 KiB, total headers to 64 KiB, and decoded request bodies to 8 MiB. Limits are checked while reading, including chunked bodies. Oversized request lines, headers, and bodies produce 414, 431, and 413 responses respectively. The optional constructor arguments maxHeaderBytes and maxBodyBytes override the total-header and body limits. Ambiguous framing, such as both Content-Length and Transfer-Encoding, is rejected rather than guessed.

The listening socket uses a 511-deep backlog by default, adjustable through the last constructor argument ($backlog); the kernel caps it at net.core.somaxconn. A larger backlog matters when many clients connect at the same moment: with a small queue the kernel drops SYNs, and TCP retransmits them after about a second, which shows up as multi-second response times in load tests.

The client reads fixed-length, chunked, and connection-close-delimited responses. Informational responses are skipped until the final response; HEAD, 204, 205, and 304 responses do not wait for a body. Truncated bodies are errors, and chunk payloads are preserved byte-for-byte. Protocol upgrades (101) and transfer codings other than chunked are not supported.

Client failures implement PSR-18's ClientExceptionInterface. Network failures use bronsted\NetworkException, invalid or unreadable outgoing requests use bronsted\RequestException, and invalid responses use bronsted\ClientException. The original request and underlying exception are available through getRequest() and getPrevious(). Outgoing request bodies must have a known size; unknown-size bodies are rejected with a request exception rather than sent without framing. Hostname resolution remains blocking.

On PHP 8.3 and newer, the server uses a bounded 1 ms non-blocking accept poll to work around Fiberloop treating listening TCP sockets as EOF. Earlier runtimes use readiness notifications. Pending socket reads are woken by Fiberloop's EOF notification when a peer closes its send direction, so no polling is needed.

Tests

Run make test to build the Docker image and run composer install and composer test inside it, or make coverage for Xdebug coverage. If you have PHP locally, composer test and composer run coverage work as well. The suite includes byte-preservation, HTTP framing, input-limit, error-isolation, and local TCP integration tests. It does not require external HTTP services.

Stress test

run.dockerfile builds an image that serves example/server.php on port 8000, and siege.dockerfile builds a Siege client image. make stress builds both, starts the server on a private Docker network, waits until it accepts connections, runs Siege against it, and then removes the container and network:

make stress                                # 250 concurrent clients for 60 s
make stress CONCURRENCY=50 DURATION=10S    # quick smoke test
make stress PHP_VERSION=8.1                # another PHP runtime

SIEGE_ARGS is passed through to Siege, for example make stress SIEGE_ARGS=-k. Siege runs in benchmark mode (-b), and the target fails if Siege reports failed transactions or the server does not start listening in time.

The images can also be used on their own:

docker build -f run.dockerfile   -t psr-http-run   .
docker build -f siege.dockerfile -t psr-http-siege .
docker run --rm -p 8000:8000 psr-http-run
docker run --rm --network host psr-http-siege -b -c 10 -t 10S http://127.0.0.1:8000/

Bugs and improvements

Report bugs in GitHub issues or feel free to make a pull request :-)

License

MIT