varion / nghttp2
PHP extension for low-level HTTP/2 primitives powered by nghttp2
Package info
github.com/varionlabs/ext-nghttp2
Language:C
Type:php-ext
Ext name:ext-nghttp2
pkg:composer/varion/nghttp2
Requires
- php: >=8.2
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:
- Read bytes from a socket.
- Pass them to
Session::receive(). - Send outbound bytes from
drainOutput(). - 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 aSession. - 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.1when--addressis not specified. - With
<PRIVATE_KEY> <CERT>, the server runs in HTTP/2 over TLS mode and requires ALPNh2. - 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
GoawayReceivedis 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
respondedguard flag. - If
DATAarrives 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.phpshows 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.phpshows 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\\SessionVarion\\Nghttp2\\SessionOptionsVarion\\Nghttp2\\RequestHeadVarion\\Nghttp2\\ResponseHead- Event hierarchy:
Varion\\Nghttp2\\Event(abstract base),Varion\\Nghttp2\\StreamEvent(abstract, hasstreamId),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(): boolhasPendingOutput(): boolgetOpenStreamCount(): intgetStreamState(int $streamId): ?string
Event class hierarchy:
Event (abstract)
|- StreamEvent (abstract, has streamId)
| |- HeadersReceived
| |- DataReceived
| |- StreamClosed
| `- StreamReset
`- ConnectionEvent (abstract)
|- GoawayReceived
|- SettingsReceived
`- SettingsAcked
Event Semantics
StreamResetrepresents a stream-level forced termination (RST_STREAM) and should be treated as an abnormal stream outcome.StreamClosedis 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::errorCodecarries the close reason from nghttp2 (0meansNO_ERROR; non-zero indicates an error condition).- Applications that need strict error handling should evaluate both events:
StreamResetfor explicit reset handling and policy decisions.StreamClosedfor 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()andnghttp2_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::strictValidationmapping 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).