QuicPro Async - PHP extension
new open source project - not explicitely for agents and LLM but will work really well in that context - made with OpenAI models
I am currently working on a PHP PECL - an extension that will add some functionality to PHP - like true async, none-blocking IO, websocket Server and a server functionality itself.
Basically it will provide all the stuff that is currently missing in PHP to make AI run smoothly.
And since it is a PECL this will also work in PHP 8.1+ - So you won’t have to wait for PHP 9 or maybe even PHP 10 - PHP internals are currently discussing the RFC for true async - and you can create streaming and websocket server without the need for externals programs (e.g. mercure for symfony).
How? I am cheating. Kind of. I make a wrapper in C that wraps quiche (written in rust - by cloudflare - therefor we can assume it is a battle tested and solid QUIC implementation - QUIC is basically providing HTTP3 - runs over UDP instead of TCP and saves us a TLS handshake per request - which means we save 40-50ms per request and many many many more things up to new TLS3 encryption) .
I am using o3 and gpt4o to build it - and a thought technique that may be best described as “like a diffusion model”.
Which means I am building a rough structure, then build the pecl, then run it, add some stubbs then write some documentation, then build a test framework (for phpunit and fuzz tests on the C code) and go deeper and deeper until I reach a point where we have the full implementation of QUIC.
I have now reached a state where I just write the documentation. The whole approach is called documentation first development.
You can read what the library / application should be capable of - you can show it around and ask for feedback while working on the code itself.
I’ll go through it in detail later - but any help is truly welcome in case you want to learn how to prompt quality code with AI.
Current status
- Core QUIC handshake, stream send/receive, epoll scheduler ✓
- PHP stubs generated, unit + C fuzz tests scaffolded ✓
- Docs & examples drafted (health-check, Fibers, WebSocket chat) ✓
- Polishing error codes + shared ticket cache docs → now
- Next up: packaging & CI for ARM + x86, Composer helpers
I had made a first version to check if it basically can do that and run a benchmark over it without tls just to check what we could possibly get speed wise. And it was at some point even 10 times faster than Curl (curl also implements quiche - marked as experimental but doesn’t provide the websocket and server functionality).
Want to play?
git clone https://github.com/intelligent-intern/quicpro_async
Here is something from the documentation:
Async I/O and Parallel Execution with quicpro_async
A detailed guide explaining how non-blocking QUIC sockets, PHP Fibers and fork-per-core workers combine to deliver low-latency and full CPU utilisation.
1 · How the pieces fit
+----------------------+ +----------------------+
| Process 1 | | Process 2 |
| (CPU core 0) | | (CPU core 1) |
| epoll loop | | epoll loop |
| Fiber A Fiber B | shared UDP :4433 port | Fiber C Fiber D |
+----------------------+ +----------------------+
↑ ↑
| shared TLS tickets (SHM-LRU) |
└───────────── 0-RTT resumes across workers ──────┘
- One process handles thousands of sockets concurrently with Fibers.
- Several processes run in parallel on different CPU cores.
- The shared-memory ticket ring lets any worker resume any client, so zero-RTT never breaks.
2 · Creating an async client
Rules: poll inside every receive loop, use short time-outs, close the session.
function fetch(string $path): string {
$s = quicpro_connect('example.org', 443);
$id = quicpro_send_request($s, $path);
while (!$r = quicpro_receive_response($s, $id)) {
quicpro_poll($s, 5); // yields ≤ 5 ms
}
quicpro_close($s);
return $r['body'];
}
$jobs = ['/a', '/b', '/c'];
$fibers = array_map(fn($p) => new Fiber(fn() => fetch($p)), $jobs);
foreach ($fibers as $f) $f->start();
foreach ($fibers as $f) echo $f->getReturn(), PHP_EOL;
3 · Single-process async server
use Quicpro\Config;
use Quicpro\Server;
use Quicpro\Session;
use Fiber;
$cfg = Config::new([
'cert_file' => 'cert.pem',
'key_file' => 'cert.pem',
'alpn' => ['h3'],
]);
$srv = new Server('[::]', 4433, $cfg);
while ($sess = $srv->accept(100)) {
(new Fiber(fn() => handle($sess)))->start();
}
function handle(Session $s): void {
$req = $s->receiveRequest();
$path = $req->headers()[':path'] ?? '/';
$stream = $req->stream();
$stream->respond(200, ['content-type' => 'text/plain']);
$stream->sendBody("echo $path\n", fin: true);
$s->close();
}
4 · Fork-per-core server (parallelism + Fibers)
use Quicpro\Config;
use Quicpro\Server;
use Fiber;
$workers = (int) trim(shell_exec('nproc')) ?: 4;
$cfg = Config::new([
'cert_file' => 'cert.pem',
'key_file' => 'cert.pem',
'alpn' => ['h3'],
'session_cache' => true // shared ticket ring
]);
for ($i = 0; $i < $workers; $i++) {
if (pcntl_fork() === 0) { worker($i, 4433, $cfg); exit; }
}
pcntl_wait($status);
function worker(int $id, int $port, Config $cfg): void {
$srv = new Server('0.0.0.0', $port, $cfg);
echo "[worker $id] ready\n";
while ($s = $srv->accept(100)) (new Fiber(fn() => handle($s, $id)))->start();
}
function handle($sess, int $wid): void {
$req = $sess->receiveRequest();
$stream = $req->stream();
$stream->respond(200, ['content-type' => 'text/plain']);
$stream->sendBody("hello from $wid\n", fin: true);
$sess->close();
}
5 · Error handling
A full catalogue of exceptions, warnings, error constants and the appropriate recovery strategy is maintained in ERROR_AND_EXCEPTION_REFERENCE.md.
Consult that file whenever a call throws or returns false
; it contains ready-to-paste try / catch
patterns and decoding of quicpro_get_last_error()
values.
6 · Performance tuning knobs
Knob | Default | When to change |
---|---|---|
Environment QUICPRO_BUSY_POLL | off | spin microseconds before yielding (ultra-low latency) |
Environment QUICPRO_XDP | empty | attach AF_XDP socket for zero-copy |
Ini quicpro.shm_size | 65536 | raise for many unique clients per second |
Config max_idle_ms | 30000 | lower for faster resource cleanup |
Busy-poll example
putenv('QUICPRO_BUSY_POLL=50');
putenv('QUICPRO_XDP=ens6');
7 · Troubleshooting flow
- Use curl --http3 -v and check for “TLS session resumed”.
- If absent, run php -i | grep quicpro.shm_size to confirm cache size.
- Increase shm_size or export / import tickets per request in FPM.
- Enable QP_TRACE=1 and inspect perf or strace output.
8 · Summary
- Fibers provide thousands of concurrent sessions inside one PHP process.
- Forked workers distribute work to every CPU core; the shared ticket ring preserves 0-RTT across processes.
- Network I/O is fully non-blocking in C; PHP code only orchestrates Fibers.
- Error details and recovery recipes live in ERROR_AND_EXCEPTION_REFERENCE.md.
- Main tuning levers: shm_size for ticket capacity and poll time-outs.
With these patterns a PHP application matches Go-level concurrency and saturates modern multi-core servers — without leaving PHP.
TLS Session Resumption & Multi-Worker Scaling
How to build a modern, fast, and scalable QUIC / HTTP-3 backend in PHP – step by step, with zero-RTT reconnects and no external load-balancer.
This document starts with the very first handshake, explains what a session ticket is, and then walks through five practical retention strategies – from single-process scripts to fork-per-core servers.
Every section includes copy-and-paste examples, plain-English background, and troubleshooting notes. Only basic PHP and cURL experience required.
0 · The problem we solve
0 .1 Why each fresh handshake steals time
A QUIC + TLS 1.3 handshake adds one extra round-trip before PHP sees the request. On a 40 ms mobile link that delay is 40 ms you could avoid for every reconnect.
0 .2 Session tickets in one sentence
The server ends the handshake with an encrypted resume-me-later blob.
The client stores that blob and sends it in the very first packet next time. Both sides can then start encrypted application data immediately – this is the famous zero-RTT.
0 .3 Why many PHP workers break tickets
With classic FPM the kernel pins each UDP flow to a single worker. A ticket issued by worker A is useless when the next packet lands on worker B.
The quicpro_async extension fixes this with a shared-memory LRU ring buffer: every worker can resume every client. Linux fans packets out automatically via SO_REUSEPORT – no proxy, no sticky sessions.
1 · Five practical ways to keep a session alive
Nr | Mode name | Best for … | How it works in practice |
---|---|---|---|
1 | Live object | long-running CLI apps, ReactPHP loops | keep the $sess resource in a global variable |
2 | Ticket cache | classic FPM, short CLI jobs | export ticket → store in APCu / Redis / file → import later |
3 | Warm container | AWS Lambda, Google Cloud Run | keep $sess in file scope; reused container = free zero-RTT |
4 | Shared-memory LRU | fork()-based multi-worker on one host | compiled-in shm cache; workers read the same ring |
5 | Stateless LB | many hosts behind anycast / layer-4 LB | client keeps ticket (mode 2); any edge can resume it |
Quick rule of thumb
• one PHP process → live object • several FPM children → ticket cache • forked workers → shared-memory LRU
2 · Compiling and configuring the shared ticket cache
2 .1 Build
The task scheduler and shared-memory ticket ring are enabled by default. No special flags are required.
phpize
./configure
make -j$(nproc)
sudo make install
2 .2 php.ini switches (environment variables still work but are optional)
; /etc/php/8.3/mods-available/quicpro_async.ini
extension = quicpro_async.so
; size of the ticket ring (bytes); 128 kB ≈ 120 tickets
quicpro.shm_size = 131072
; custom shm_open() name when several independent servers run
quicpro.shm_path = /quicpro_ring
; choose retention mode: 0=AUTO 1=LIVE 2=TICKET 3=WARM 4=SHM_LRU
quicpro.session_mode = 0
2 .3 Session-mode constants available in PHP
QUICPRO_SESSION_AUTO (0)
QUICPRO_SESSION_LIVE (1)
QUICPRO_SESSION_TICKET (2)
QUICPRO_SESSION_WARM (3)
QUICPRO_SESSION_SHM_LRU (4)
Override php.ini for a single server:
$cfg = Quicpro\Config::new([
'session_mode' => QUICPRO_SESSION_TICKET,
'alpn' => ['h3'],
]);
AUTO chooses the smartest mode at runtime:
- live object when the process never exits
- ticket cache under FPM
- warm when a FaaS variable is detected
- shm-LRU after fork() when shm cache is available
3 · Hands-on: zero-RTT echo server with one worker per core
The script below forks N children (default = number of CPU cores), binds them all to UDP :4433, and resumes tickets across workers. Each worker processes sessions inside a Fiber so long polls never block new handshakes.
examples/ticket_resumption_multiworker.php
<?php
declare(strict_types=1);
use Quicpro\Config;
use Quicpro\Server;
use Quicpro\Session;
use Fiber;
/* choose worker count */
$workers = (int) ($argv[1] ?? trim(shell_exec('nproc'))) ?: 4;
$port = 4433;
/* shared config */
$cfg = Config::new([
'cert_file' => __DIR__ . '/../certs/server.pem',
'key_file' => __DIR__ . '/../certs/server.key',
'alpn' => ['h3'],
'session_cache' => true, // shared tickets
]);
echo "Spawning {$workers} workers on UDP :{$port}\n";
/* master forks */
for ($i = 0; $i < $workers; $i++) {
if (pcntl_fork() === 0) { worker($i, $port, $cfg); exit; }
}
pcntl_wait($status);
/* worker loop */
function worker(int $wid, int $port, Config $cfg): void
{
$srv = new Server('0.0.0.0', $port, $cfg); /* SO_REUSEPORT implicit */
echo "[worker {$wid}] ready\n";
while ($sess = $srv->accept(100)) {
(new Fiber(fn() => handle($sess, $wid)))->start();
}
}
function handle(Session $s, int $wid): void
{
$req = $s->receiveRequest();
$path = $req->headers()[':path'] ?? '/';
$stream = $req->stream();
$stream->respond(200, ['content-type' => 'text/plain']);
$stream->sendBody("echo from worker {$wid}: {$path}\n", fin: true);
$s->close();
}
3 .1 Run
sudo setcap cap_net_bind_service=+ep $(command -v php)
php examples/ticket_resumption_multiworker.php 4
3 .2 Observe zero-RTT across workers
curl --http3 -k https://localhost:4433/foo
curl --http3 -k https://localhost:4433/bar -v | grep session
# should print “TLS session resumed” and a worker ID that may differ
4 · Where each mode shines in real projects
- API gateway with dozens of upstream sessions → live object
- Classical FPM web app → ticket cache (APCu is enough)
- Lambda function with 200 ms bursts → warm container
- Chat backend on a 16-core box → shared-memory LRU
- Anycast edge nodes worldwide → client ticket cache + stateless servers
You can mix modes; they work independently.
5 · Troubleshooting
A full catalogue of exceptions, warnings, error constants and recovery recipes is maintained in ERROR_AND_EXCEPTION_REFERENCE.md. Consult that file whenever a call throws or returns false.
6 · Cheat-sheet to pin on your monitor
# compile (ticket ring is built-in)
phpize
./configure
make -j$(nproc)
sudo make install
# php.ini quick setup
quicpro.shm_size = 131072
quicpro.session_mode = 4 ; force shared-memory LRU
# run eight workers on UDP :4433
php examples/ticket_resumption_multiworker.php 8
Now you know what session tickets are, why they cut latency, and how to keep them alive in every PHP deployment scenario – from a single CLI script to an eight-core QUIC server that scales without a load-balancer. Enjoy your instant handshakes.
Usage Examples
1 · Single GET request (synchronous)
Why?
Health-checks, one-shot CLI scripts, cron jobs, or any task where you
need exactly one HTTP/3 request and do not care about overlap or
latency hiding.
$sess = quicpro_connect('example.org', 443);
$stream = quicpro_send_request($sess, '/');
while (!$resp = quicpro_receive_response($sess, $stream)) {
quicpro_poll($sess, 20); // blocks ≤ 20 ms per loop
}
echo $resp['body'];
quicpro_close($sess);
2 · Parallel GETs with Fibers (concurrent client)
Why?
Scrape three pages in parallel during a CMS warm-up, pre-render a trio
of micro-frontends, or fetch several API endpoints concurrently in one
CLI process.
function fetch(string $path): string {
$s = quicpro_connect('example.org', 443);
$id = quicpro_send_request($s, $path);
while (!$r = quicpro_receive_response($s, $id)) quicpro_poll($s, 5);
quicpro_close($s);
return $r['body'];
}
$jobs = ['/a', '/b', '/c'];
$fibers = array_map(fn($p) => new Fiber(fn() => fetch($p)), $jobs);
foreach ($fibers as $f) $f->start();
foreach ($fibers as $f) echo $f->getReturn(), PHP_EOL;
3 · Streaming POST upload (chunked body)
Why?
Send a multi-gigabyte file from disk to an object-storage gateway without
loading it entirely into memory. The body is streamed in 16 kB chunks,
and the loop never blocks longer than one system call.
$s = quicpro_connect('uploader.local', 8443);
$sid = quicpro_send_request(
$s, '/upload', headers: ['content-type' => 'application/octet-stream']
);
$file = fopen('dump.raw', 'rb');
while (!feof($file)) {
quiche_h3_send_body_chunk($s, $sid, fread($file, 16 * 1024));
quicpro_poll($s, 0); // non-blocking
}
quiche_h3_finish_body($s, $sid);
while (!$r = quicpro_receive_response($s, $sid)) quicpro_poll($s, 10);
print_r($r);
4 · WebSocket chat client (HTTP-3 CONNECT)
Why?
Build a thin chat front-end, subscribe to an event stream, or interact
with a push API where the server talks back at arbitrary times.
$sess = quicpro_connect('chat.example', 443);
$ws = $sess->upgrade('/chat'); // RFC 9220 CONNECT → WS
fwrite($ws, "hello from PHP\n");
while (!feof($ws)) echo "Server: ", fgets($ws);
5 · Minimal WebSocket server with Fibers (≤ 5 channels)
Why?
Prototype a low-traffic WebSocket endpoint—e.g. a who-is-online status
panel—without deploying Nginx Unit or Go. The Fiber budget caps active
chats at five, so RAM usage stays predictable.
use Quicpro\Config;
use Quicpro\Server;
use Quicpro\Session;
use Fiber;
$cfg = Config::new(['cert_file' => 'cert.pem', 'key_file' => 'key.pem']);
$server = new Server('::', 4433, $cfg);
$handle = function (Session $sess): void {
static $open = 0;
if ($open >= 5 || !$sess->isWebSocket('/chat')) { $sess->close(); return; }
$ws = $sess->upgrade('/chat');
$open++;
while (!feof($ws)) fwrite($ws, strtoupper(fgets($ws)));
fclose($ws); $sess->close(); $open--;
};
while ($c = $server->accept()) (new Fiber($handle))->start($c);
6 · Warm-start in FaaS / serverless containers
Why?
Cut cold-start pain on platforms like AWS Lambda or Google Cloud Run: the
global $GLOBAL_SESS
survives between invocations so repeat calls skip
both TCP and TLS handshakes.
/* Global survives container reuse */
$GLOBAL_SESS = null;
function handler(array $event): string {
global $GLOBAL_SESS;
$GLOBAL_SESS ??= quicpro_connect('api.internal', 4433);
$id = quicpro_send_request($GLOBAL_SESS, '/data?id=' . $event['id']);
while (!$r = quicpro_receive_response($GLOBAL_SESS, $id)) {
quicpro_poll($GLOBAL_SESS, 5);
}
return $r['body'];
}
7 · Collecting stats & QLOG traces
Why?
Debug congestion control, retransmissions, or handshake details. QLOG is
the official QUIC trace format; tools like qvis and Wireshark read it.
$s = quicpro_connect('example.org', 443);
quicpro_set_qlog_path($s, '/tmp/example.qlog'); // live trace
$id = quicpro_send_request($s, '/');
while (!quicpro_receive_response($s, $id)) quicpro_poll($s, 10);
print_r(quicpro_get_stats($s));
quicpro_close($s);
Visualise the file with qvis.dev or Wireshark.
8 · Zero-copy XDP path + busy-poll tuning
Why?
Push latency to microsecond levels on dedicated hardware—think
high-frequency trading or telemetry bursts—by avoiding both syscalls and
kernel queues.
putenv('QUICPRO_XDP=ens6'); // AF_XDP socket
putenv('QUICPRO_BUSY_POLL=50'); // spin 50 µs before Fiber yield
$s = quicpro_connect('10.0.0.2', 4433);
$id = quicpro_send_request($s, '/ping');
while (!quicpro_receive_response($s, $id)) quicpro_poll($s, 0); // busy-poll
$lat = quicpro_get_stats($s)['rtt'];
printf("RTT: %.1f µs\n", $lat / 1000);
quicpro_close($s);
AI Agent Examples
1 · Streaming chat completion (Server-Sent Events)
Why?
Stream tokens from an LLM back-end (e.g. OpenAI, Ollama, vLLM) to the
browser without waiting for the full answer.
$sess = quicpro_connect('llm.internal', 4433);
$body = json_encode([
'model' => 'gpt-4o',
'stream' => true,
'messages' => [['role'=>'user','content'=>'Hello, who won the 2022 World Cup?']]
]);
$hdrs = ['content-type'=>'application/json'];
$id = quicpro_send_request($sess, '/v1/chat/completions', $hdrs, $body);
while (!$r = quicpro_receive_response($sess, $id)) {
quicpro_poll($sess, 10); // max 10 ms latency
}
foreach (explode("\n", $r['body']) as $line) {
if (str_starts_with($line, 'data: ')) {
echo substr($line, 6); // stream token to stdout
}
}
quicpro_close($sess);
2 · Ten parallel prompts with Fibers
Why?
Bulk-score a list of product reviews, extract keywords, or translate many
sentences in a single PHP worker without blocking.
function ask(string $prompt): string {
$s = quicpro_connect('llm.internal', 4433);
$id = quicpro_send_request($s, '/v1/completions',
['content-type'=>'application/json'],
json_encode(['model'=>'gpt-4o','prompt'=>$prompt])
);
while (!$r = quicpro_receive_response($s, $id)) quicpro_poll($s, 5);
quicpro_close($s);
return json_decode($r['body'], true)['choices'][0]['text'];
}
$prompts = array_map(fn($i)=>"Summarise text $i", range(1,10));
$fibers = array_map(fn($p)=>new Fiber(fn() => ask($p)), $prompts);
foreach ($fibers as $f) $f->start();
foreach ($fibers as $idx=>$f) echo "#$idx → ", $f->getReturn(), PHP_EOL;
3 · Bidirectional WebSocket to a local RAG pipeline
Why?
Keep a long-lived connection to a Retrieval-Augmented-Generation server
so each query reuses the same context cache and avoids TLS overhead.
$sess = quicpro_connect('rag.local', 8443);
$ws = $sess->upgrade('/chat'); // RFC 9220 CONNECT → WS
fwrite($ws, json_encode(['ask'=>'What is E-MC²?'])."\n");
while (!feof($ws)) {
$msg = fgets($ws);
echo "Agent says: ", $msg;
if (str_contains($msg, '[DONE]')) break;
}
fclose($ws);
$sess->close();
4 · Fork-per-core agent gateway (zero-RTT + Fibers)
Why?
Expose many AI models behind one UDP port; each worker process handles its
own epoll loop while the shared ticket ring preserves 0-RTT reconnects.
use Quicpro\Config;
use Quicpro\Server;
use Fiber;
$cores = (int) trim(shell_exec('nproc')) ?: 4;
$cfg = Config::new([
'cert_file'=>'cert.pem', 'key_file'=>'key.pem',
'alpn'=>['h3'], 'session_cache'=>true
]);
for ($i = 0; $i < $cores; $i++) {
if (pcntl_fork() === 0) { serve($i, 4433, $cfg); exit; }
}
pcntl_wait($s);
function serve(int $wid, int $port, Config $cfg): void {
$srv = new Server('0.0.0.0', $port, $cfg);
echo "[worker $wid] online\n";
while ($s = $srv->accept(50)) (new Fiber(fn() => route($s)))->start();
}
function route($sess): void {
$req = $sess->receiveRequest();
$path = $req->headers()[':path'] ?? '/';
match ($path) {
'/v1/chat/completions' => proxy_to('llm1.internal', $sess, $req),
'/v1/embeddings' => proxy_to('embed.internal', $sess, $req),
default => $sess->close()
};
}
function proxy_to(string $backend, $sess, $req): void {
/* left as exercise: send to backend, stream back to client */
$sess->close();
}
5 · Invoking a Model Context Protocol (MCP) tool over HTTP/3
Why?
MCP is the new open standard (adopted by Anthropic and OpenAI) that lets
any LLM call external tools through one uniform interface.
When your PHP app can hit an MCP server you gain model-agnostic tool
invocation: Claude, GPT-4o, or an open-source model can all run the same
calculator, database, or vector-search function without custom glue.
Scenario
You run a remote MCP server that exposes a calculator
tool via HTTP
Server-Sent Events (SSE). The goal is to ask the tool “sqrt(144)” and
stream the result back as soon as it is ready.
use Fiber;
/* 1. open one QUIC session to the MCP server */
$sess = quicpro_connect('mcp.example', 4433);
/* 2. MCP servers use SSE for streaming responses */
/* POST body = JSON with tool name + arguments */
$body = json_encode([
'tool' => 'calculator',
'input' => ['expression' => 'sqrt(144)'],
'stream' => true // ask for streaming
]);
$hdrs = [
'content-type' => 'application/json',
'accept' => 'text/event-stream',
];
/* 3. send the request */
$stream = quicpro_send_request($sess, '/mcp/v1/call_tool', $hdrs, $body);
/* 4. read Server-Sent Events as they come in */
/* every event line starts with “data: {JSON…}” */
while (true) {
$resp = quicpro_receive_response($sess, $stream);
if ($resp) break; // final chunk received
quicpro_poll($sess, 5); // yield ≤ 5 ms
}
/* 5. split the SSE payload into lines, show only “data: …” */
foreach (explode("\n", $resp['body']) as $line) {
if (str_starts_with($line, 'data: ')) {
$event = json_decode(substr($line, 6), true);
printf("Δ %s\n", $event['chunk'] ?? $event['result'] ?? '');
}
}
quicpro_close($sess);
Output
Δ 1 Δ 2 Δ . Δ 0
The tool streamed four chunks: “1”, “2”, “.”, “0”. Concatenated they give
12.0 – the square root of 144.
Error handling cheat-sheet
Most failures map to the standard ERROR_AND_EXCEPTION_REFERENCE.md
.
Situation | Likely exception / error code | Quick fix |
---|---|---|
Tool name typo | QuicErrorUnknownFrame / 4xx JSON |
Check tool field spelling |
MCP server unreachable | QuicSocketException |
Verify DNS / firewall |
Bad JSON in request | QuicTransportException (HTTP 400) |
json_encode -safe and re-send |
SSE disconnect mid-stream | QuicStreamException |
Retry idempotent call or fall back |
These five examples now cover single-shot requests, parallel Fibers,
stream uploads, WebSockets, fork-per-core gateways and the new MCP
tool standard – everything you need to wire PHP agents to modern LLM
stacks.
QuicPro Async – Error-Handling Reference
This document lists all runtime exceptions, warnings, PHP errors, and error-code constants exposed by the quicpro_async extension.
1 · Exception Classes
Class name | Thrown when … |
---|---|
QuicConnectionException |
a QUIC connection cannot be created or maintained |
QuicStreamException |
stream creation, read, write, or shutdown fails |
QuicTlsHandshakeException |
TLS 1.3 handshake fails (invalid certificate, timeout, etc.) |
QuicTimeoutException |
any operation exceeds its deadline |
QuicTransportException |
QUIC protocol violation (e.g. invalid frame, version mismatch) |
QuicServerException |
server startup or runtime failure (port already in use, bind error) |
QuicClientException |
client-side connection or response failure |
QuicSocketException |
generic network/socket issue (unreachable, ECONNREFUSED, etc.) |
Usage example
try {
$sess = quicpro_connect('bad.host', 443);
} catch (QuicConnectionException $e) {
error_log("connect failed: " . $e->getMessage());
}
2 · Runtime Warnings
Warning constant | Situation |
---|---|
QuicWarningInvalidParameters |
invalid options provided to a function / constructor |
QuicWarningConnectionSlow |
RTT or handshake takes unusually long; timeout imminent |
QuicWarningDeprecatedFeature |
caller uses a soon-to-be-removed API flag |
QuicWarningPartialData |
a datagram / stream frame was only partially processed |
Warnings are emitted with trigger_error(..., E_WARNING)
and do not throw.
3 · PHP Error Messages
Severity | Example message |
---|---|
E_WARNING |
Connection handshake took too long |
E_WARNING |
Received unknown QUIC frame type 0x1f |
E_ERROR |
Unable to initialize QUIC context |
E_ERROR |
TLS handshake failed: invalid certificate |
E_ERROR
terminates the request; E_WARNING
can be silenced or converted to an exception via ErrorException
.
4 · Error-Code Constants
Constant | Value | Meaning |
---|---|---|
QUIC_ERROR_OK |
0 |
success |
QUIC_ERROR_TIMEOUT |
1 |
operation timed out |
QUIC_ERROR_INVALID_STATE |
2 |
function called in wrong session/stream state |
QUIC_ERROR_CONNECTION_LOST |
3 |
connection aborted unexpectedly |
QUIC_ERROR_HANDSHAKE_FAILED |
4 |
TLS handshake error |
QUIC_ERROR_STREAM_ERROR |
5 |
unrecoverable stream failure |
QUIC_ERROR_TRANSPORT_ERROR |
6 |
QUIC transport-level protocol error |
QUIC_ERROR_INTERNAL |
7 |
internal failure (e.g. OOM) |
QUIC_ERROR_UNSUPPORTED_VERSION |
8 |
QUIC version not supported by peer |
Return-style APIs (e.g. quicpro_poll()
) expose these codes via quicpro_get_last_error()
.
5 · Quick Reference Cheat-Sheet
- Always catch the eight exception classes for robust error handling.
- Monitor warnings in logs to spot slow joins, bad parameters, and deprecated flags.
- Treat
E_ERROR
as fatal; design graceful degradation aroundE_WARNING
. - Map numeric codes from
quicpro_get_last_error()
to the table above.
$sess = quicpro_connect('api.internal', 443);
$id = quicpro_send_request($sess, '/data');
while (!quicpro_receive_response($sess, $id)) {
if (!quicpro_poll($sess, 10)) {
throw new RuntimeException('poll failed: ' . quicpro_get_last_error());
}
}
Using the documentation first approach I can basically write the unittests based on that - e.g. having one per desired Exception or Error, then add benchmark tests (will use pulumi to run a couple servers and then start a couple hundret thousand agents in parallel and we’ll see what happens…
When all the tests are written I can implement the C code - or let’s say go deeper and fix what is wrong at the moment. The whole project is way to complex to give it to a model and tell it to make an outline and implement it. It takes context to make good code.
And deep research also is far away from getting that.
But I am sure we can build an agentic network that runs extremly fast based on this lib to change that