varion/nghttp2

PHP extension for low-level HTTP/2 primitives powered by nghttp2

Maintainers

Package info

github.com/varionlabs/ext-nghttp2

Language:C

Type:php-ext

Ext name:ext-nghttp2

pkg:composer/varion/nghttp2

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-main 2026-03-09 22:09 UTC

This package is auto-updated.

Last update: 2026-04-09 22:23:21 UTC


README

This extension exposes nghttp2 as a Sans-I/O engine for PHP under the Varion\\Nghttp2 namespace.

The extension does not perform socket I/O itself. Applications feed inbound bytes via receive() and collect outbound bytes via drainOutput(). Protocol events are then consumed using nextEvent().

Quick Start

Install via PIE

pie install varion/nghttp2
php -m | grep nghttp2

If you prefer enabling it persistently, add this to your php.ini:

extension=nghttp2

Uninstall:

pie uninstall varion/nghttp2

Build from Source

phpize
./configure --enable-nghttp2
make -j"$(nproc)"

Enable from Build Output

php -d extension=$(pwd)/modules/nghttp2.so -m | grep nghttp2

Why Sans-I/O?

Traditional HTTP libraries combine protocol logic with socket handling.

This extension separates them.

Benefits:

  • easier integration with different event loops
  • easier testing of protocol logic
  • clearer separation between transport and HTTP/2 protocol layers

Architecture

This extension exposes the nghttp2 HTTP/2 state machine to PHP while keeping all socket I/O outside the extension.

The extension operates as a Sans-I/O protocol engine.

Applications are responsible for:

  • Transport (TCP/TLS sockets)
  • Event loop integration
  • Backpressure control
  • Connection lifecycle

The extension is responsible for:

  • HTTP/2 frame encoding and decoding
  • Stream state machines
  • Flow control
  • Protocol event generation (nextEvent())

Integration with Event Loops

Because the extension follows a Sans-I/O model, it can be integrated with different event loop implementations.

Possible integrations include:

  • ReactPHP
  • Amp
  • custom polling loops
  • future PHP polling/event APIs

Typical integration pattern:

  1. Read bytes from a socket.
  2. Pass them to Session::receive().
  3. Send outbound bytes from drainOutput().
  4. Process events using nextEvent().

This design keeps transport concerns separate from the HTTP/2 protocol engine.

Transport Model

This extension does not manage network sockets.

Applications must provide a transport layer, typically:

  • TCP sockets
  • TLS streams
  • event-loop driven transports

Example responsibilities of the application:

  • TLS negotiation
  • ALPN validation
  • socket read/write loops
  • connection lifetime management

The extension focuses on HTTP/2 protocol processing and state transitions.

Quick Check

php -d extension=$(pwd)/modules/nghttp2.so examples/session_basic.php
php -d extension=$(pwd)/modules/nghttp2.so examples/client-minimal.php
php -d extension=$(pwd)/modules/nghttp2.so examples/server-minimal.php 8080 --address=127.0.0.1

Minimal API Example

<?php

use Varion\Nghttp2\Session;

$client = new Session(Session::ROLE_CLIENT);
$server = new Session(Session::ROLE_SERVER);

foreach ($client->drainOutput() as $chunk) {
    $server->receive($chunk);
}

while ($event = $server->nextEvent()) {
    var_dump(get_class($event));
}

Client Example

examples/client-minimal.php is a minimal HTTP/2 client example using a real TLS connection.

Run

php -d extension=$(pwd)/modules/nghttp2.so examples/client-minimal.php

What It Demonstrates

  • TLS + ALPN (h2) negotiation before creating a Session.
  • Sans-I/O loop pattern: receive() -> drainOutput() -> nextEvent().
  • Stream-event-oriented response handling (HeadersReceived, DataReceived, StreamReset, StreamClosed).
  • Header block collection that keeps multiple HEADERS blocks (including possible trailers).

Simplifications

  • The example intentionally focuses on stream-level response handling.
  • Connection-level events (for example GoawayReceived) are intentionally omitted in this minimal client.

Server Example

examples/server-minimal.php is a minimal event-loop server example for the extension.

CLI Syntax

php -d extension=$(pwd)/modules/nghttp2.so examples/server-minimal.php <PORT> [<PRIVATE_KEY> <CERT>] [--address=<ADDR>]
  • Default address is 127.0.0.1 when --address is not specified.
  • With <PRIVATE_KEY> <CERT>, the server runs in HTTP/2 over TLS mode and requires ALPN h2.
  • Without key/cert, the server runs in cleartext HTTP/2 (h2c, prior knowledge).

Launch Examples

# TLS mode (HTTP/2 over TLS)
php -d extension=$(pwd)/modules/nghttp2.so examples/server-minimal.php 8443 ./localhost-key.pem ./localhost.pem --address=127.0.0.1

# h2c mode (HTTP/2 cleartext prior knowledge)
php -d extension=$(pwd)/modules/nghttp2.so examples/server-minimal.php 8080 --address=127.0.0.1

curl Test Examples

# TLS
curl --http2 -k -v https://127.0.0.1:8443/

# h2c
curl --http2-prior-knowledge -v http://127.0.0.1:8080/

Note: h2c in this example expects prior knowledge clients (for example curl --http2-prior-knowledge).

Simplifications and Defensive Behavior

  • For simplicity, the server example ends connection processing when GoawayReceived is observed (after one final output flush).
  • Request trailers are preserved for inspection, but response decisions use the first request header block (initial_headers).
  • The example enforces one response per stream with a responded guard flag.
  • If DATA arrives before request headers, the server logs it as an unexpected order and continues with a minimal fallback path.

Preface Bootstrap Examples

Preface-focused examples are available as protocol bootstrap references:

php -d extension=$(pwd)/modules/nghttp2.so examples/client_preface.php
php -d extension=$(pwd)/modules/nghttp2.so examples/server_preface.php
  • examples/client_preface.php shows how a client session emits initial HTTP/2 bytes (client preface and initial SETTINGS) and how to pass them to a peer session.
  • examples/server_preface.php shows how a server session consumes the client preface path and produces initial server-side protocol output/events.
  • Use these files when you want to study protocol startup behavior in isolation, before reading the full event-loop examples.

Known Limitations

  • These examples are intentionally minimal and are not production-ready HTTP server/client implementations.
  • The server example uses a single-process, blocking event loop for clarity.
  • Advanced production concerns (resource limits, backpressure tuning, graceful shutdown orchestration, and comprehensive observability) are out of scope for the examples.

Purpose

  • Keep socket I/O and event loops out of the extension, and control the HTTP/2 state machine from PHP.
  • Use receive() / drainOutput() / nextEvent() as the core API.
  • Separate transport concerns so integration with ReactPHP or future polling APIs stays straightforward.

Current Scope

  • Varion\\Nghttp2\\Session
  • Varion\\Nghttp2\\SessionOptions
  • Varion\\Nghttp2\\RequestHead
  • Varion\\Nghttp2\\ResponseHead
  • Event hierarchy: Varion\\Nghttp2\\Event (abstract base), Varion\\Nghttp2\\StreamEvent (abstract, has streamId), Varion\\Nghttp2\\ConnectionEvent (abstract)
  • Concrete events under Varion\\Nghttp2\\Events: stream events (HeadersReceived, DataReceived, StreamClosed, StreamReset) and connection events (GoawayReceived, SettingsReceived, SettingsAcked)
  • Exception classes (Exception, RuntimeException, ProtocolException)
  • Minimal debugging/testing helpers:
    • hasPendingEvents(): bool
    • hasPendingOutput(): bool
    • getOpenStreamCount(): int
    • getStreamState(int $streamId): ?string

Event class hierarchy:

Event (abstract)
|- StreamEvent (abstract, has streamId)
|  |- HeadersReceived
|  |- DataReceived
|  |- StreamClosed
|  `- StreamReset
`- ConnectionEvent (abstract)
   |- GoawayReceived
   |- SettingsReceived
   `- SettingsAcked

Event Semantics

  • StreamReset represents a stream-level forced termination (RST_STREAM) and should be treated as an abnormal stream outcome.
  • StreamClosed is the terminal lifecycle notification for a stream. It is emitted when nghttp2 reports stream closure, regardless of whether the closure was clean or error-driven.
  • StreamClosed::errorCode carries the close reason from nghttp2 (0 means NO_ERROR; non-zero indicates an error condition).
  • Applications that need strict error handling should evaluate both events:
    • StreamReset for explicit reset handling and policy decisions.
    • StreamClosed for final completion state and close reason inspection.
  • Some behaviors in bundled examples are intentionally simplified policy choices (for example fail-fast stream handling and GOAWAY shutdown flow), not hard API contract requirements.

Design Principles

  • Do not expose callback registration to PHP users; convert callbacks into an internal event queue.
  • Use nghttp2_session_mem_recv() and nghttp2_session_send().
  • Collect outbound bytes via drainOutput().
  • Consume protocol events via nextEvent().
  • Keep introspection minimal in the first release; do not provide a full visualization/debug API yet.

Future Direction

This project focuses on exposing the HTTP/2 protocol engine.

Future experiments may explore similar Sans-I/O bindings for HTTP/3 over QUIC.

However, HTTP/3 requires a different transport architecture and is not currently in scope for this extension. This extension remains focused on HTTP/2.

TODO / Not Implemented

  • SessionOptions::strictValidation mapping to nghttp2 options.
  • Advanced header normalization.
  • Stream list dumps, detailed window-size visibility, frame history, timeline trace.
  • Large debug visualization APIs such as debug snapshots (can be added in a separate layer later).