Request handlers
Examples
Every example of this topic, with its output.
ClientRequest#
use Swerve\ClientRequest;
use Swerve\RequestHandler;
$app = new RequestHandler(function (ClientRequest $request) {
$name = $request->getRequestHeaders()['x-name'][0] ?? 'world';
$request->sendResponseHeaders(200, ['content-type' => 'text/plain']);
$request->write("Hello, $name: {$request->getMethod()} {$request->getTarget()}\n");
});
if (debug_backtrace()) {
return $app; // swerve loads this file: it is the application's swerve.php
}
// Run directly: serve the application with swerve, and send it a request
require __DIR__ . '/../serve.php';
serve(__FILE__, ["GET /greeting?lang=en HTTP/1.1\r\nHost: example\r\nX-Name: Ann\r\n\r\n"]);Output
HTTP/1.1 200 OK content-type: text/plain Server: Swerve Transfer-Encoding: chunked Connection: close 22 Hello, Ann: GET /greeting?lang=en 0
ClientRequest#
return new Swerve\RequestHandler(function (Swerve\ClientRequest $request) {
$request->sendResponseHeaders(200, ['content-type' => 'text/event-stream']);
foreach ([1, 2, 3] as $n) {
$request->write("data: $n\n\n");
$request->flush();
phasync::sleep(1);
}
});ClientRequest::sendResponseHeaders#
use Swerve\ClientRequest;
use Swerve\RequestHandler;
$app = new RequestHandler(function (ClientRequest $request) {
// A 1xx goes out at once: here 103 Early Hints, while the page is still being made
$request->sendResponseHeaders(103, ['link' => '</style.css>; rel=preload; as=style']);
// The final head waits for the first body bytes, and goes out with them
$request->sendResponseHeaders(201, ['content-type' => 'application/json', 'location' => '/items/7']);
$request->write('{"id":7}');
});
if (debug_backtrace()) {
return $app; // swerve loads this file: it is the application's swerve.php
}
// Run directly: serve the application with swerve, and send it requests
require __DIR__ . '/../serve.php';
serve(__FILE__, ["POST /items HTTP/1.1\r\nHost: example\r\nContent-Length: 0\r\n\r\n"]);Output
HTTP/1.1 103 Early Hints
link: </style.css>; rel=preload; as=style
HTTP/1.1 201 Created
content-type: application/json
location: /items/7
Server: Swerve
Transfer-Encoding: chunked
Connection: close
8
{"id":7}
0ClientRequest::flush#
use Swerve\ClientRequest;
use Swerve\RequestHandler;
$app = new RequestHandler(function (ClientRequest $request) {
$request->sendResponseHeaders(200, ['content-type' => 'text/plain']);
$request->flush(); // the client sees the head now, before any work is done
foreach (['parsing', 'indexing', 'done'] as $step) {
phasync::sleep(0.05); // the work
$request->write("$step\n"); // each write is a chunk on the wire
}
});
if (debug_backtrace()) {
return $app; // swerve loads this file: it is the application's swerve.php
}
// Run directly: serve the application with swerve, and send it requests
require __DIR__ . '/../serve.php';
serve(__FILE__, ["GET /rebuild HTTP/1.1\r\nHost: example\r\n\r\n"]);Output
HTTP/1.1 200 OK content-type: text/plain Server: Swerve Transfer-Encoding: chunked Connection: close 8 parsing 9 indexing 5 done 0
ClientRequest::sendFile#
use Swerve\ClientRequest;
use Swerve\RequestHandler;
$app = new RequestHandler(function (ClientRequest $request) {
$file = fopen('php://temp', 'w+');
fwrite($file, "0123456789abcdefghij");
// No content-length is computed: give one with the head, or the body is chunked
$request->sendResponseHeaders(200, ['content-type' => 'text/plain', 'content-length' => '6']);
$request->sendFile($file, 10, 6); // 6 bytes from offset 10
fclose($file);
});
if (debug_backtrace()) {
return $app; // swerve loads this file: it is the application's swerve.php
}
// Run directly: serve the application with swerve, and send it requests
require __DIR__ . '/../serve.php';
serve(__FILE__, ["GET /file HTTP/1.1\r\nHost: example\r\n\r\n"]);Output
HTTP/1.1 200 OK content-type: text/plain Server: Swerve Content-Length: 6 Connection: close abcdef
ClientRequest::end#
use Swerve\ClientRequest;
use Swerve\RequestHandler;
$app = new RequestHandler(function (ClientRequest $request) {
// Trailers: named in the head, sent after a chunked body, for what is known only at the end
$request->sendResponseHeaders(200, ['content-type' => 'text/plain', 'trailer' => 'x-checksum']);
$hash = hash_init('crc32b');
foreach (['one ', 'two ', 'three'] as $part) {
$request->write($part);
hash_update($hash, $part);
}
$request->end(['x-checksum' => hash_final($hash)]);
});
if (debug_backtrace()) {
return $app; // swerve loads this file: it is the application's swerve.php
}
// Run directly: serve the application with swerve, and send it requests
require __DIR__ . '/../serve.php';
serve(__FILE__, ["GET / HTTP/1.1\r\nHost: example\r\nTE: trailers\r\n\r\n"]);Output
HTTP/1.1 200 OK content-type: text/plain trailer: x-checksum Server: Swerve Transfer-Encoding: chunked Connection: close 4 one 4 two 5 three 0 x-checksum: 7825d3d7
HeadersSentException#
use Swerve\ClientRequest;
use Swerve\HeadersSentException;
use Swerve\RequestHandler;
$app = new RequestHandler(function (ClientRequest $request) {
$request->write("partial output\n"); // commits an implicit 200
try {
$request->sendResponseHeaders(500);
} catch (HeadersSentException) {
$request->write("too late for a 500: the head is out\n");
}
});
if (debug_backtrace()) {
return $app; // swerve loads this file: it is the application's swerve.php
}
// Run directly: serve the application with swerve, and send it requests
require __DIR__ . '/../serve.php';
serve(__FILE__, ["GET / HTTP/1.1\r\nHost: example\r\n\r\n"]);Output
HTTP/1.1 200 OK Server: Swerve Transfer-Encoding: chunked Connection: close f partial output 24 too late for a 500: the head is out 0
FormBody#
use phasync\Psr\StringStream;
use Swerve\Psr\FormBody;
// A url-encoded POST body, as a browser's form sends it
$body = new StringStream('name=Ann+Lee&tags[]=php&tags[]=async&user.age=42');
$form = FormBody::for('POST', 'application/x-www-form-urlencoded', $body);
var_export($form->fields()); // what $_POST would hold: a.b becomes a_b, as in PHP
echo "\n";Output
array (
'name' => 'Ann Lee',
'tags' =>
array (
0 => 'php',
1 => 'async',
),
'user_age' => '42',
)FormBody::for#
use phasync\Psr\StringStream;
use Swerve\Psr\FormBody;
// PHP parses a POST with a form's Content-Type; every other body stays raw
$requests = [
['POST', 'application/x-www-form-urlencoded'],
['POST', 'multipart/form-data; boundary=x'],
['POST', 'application/json'],
['PUT', 'application/x-www-form-urlencoded'],
];
foreach ($requests as [$method, $type]) {
$form = FormBody::for($method, $type, new StringStream(''));
echo str_pad("$method $type", 45), null === $form ? 'raw' : 'form', "\n";
}Output
POST application/x-www-form-urlencoded form POST multipart/form-data; boundary=x form POST application/json raw PUT application/x-www-form-urlencoded raw
FormBody::files#
use phasync\Psr\StringStream;
use Swerve\Psr\FormBody;
$body = "--b\r\n"
. "Content-Disposition: form-data; name=\"title\"\r\n\r\n"
. "Holiday\r\n"
. "--b\r\n"
. "Content-Disposition: form-data; name=\"photos[]\"; filename=\"beach.txt\"\r\n"
. "Content-Type: text/plain\r\n\r\n"
. "sand and sea\r\n"
. "--b--\r\n";
$form = FormBody::for('POST', 'multipart/form-data; boundary=b', new StringStream($body));
var_export($form->fields());
echo "\n";
$photo = $form->files()['photos'][0]; // a PSR-7 UploadedFileInterface
echo $photo->getClientFilename(), ', ', $photo->getClientMediaType(), ', ', $photo->getSize(), " bytes\n";
$target = sys_get_temp_dir() . '/beach-' . getmypid() . '.txt';
$photo->moveTo($target); // kept; files not moved are deleted with the form
echo file_get_contents($target), "\n";
unlink($target);Output
array ( 'title' => 'Holiday', ) beach.txt, text/plain, 12 bytes sand and sea
FormBody::input#
use phasync\Psr\StringStream;
use Swerve\Psr\FormBody;
$form = FormBody::for('POST', 'application/x-www-form-urlencoded', new StringStream('a=1&b=2'));
var_export($form->fields());
echo "\n";
echo $form->input()->getContents(), "\n"; // php://input: still readable after parsingOutput
array ( 'a' => '1', 'b' => '2', ) a=1&b=2
RequestBody#
use Swerve\ClientRequest;
use Swerve\Psr\RequestBody;
use Swerve\RequestHandler;
$app = new RequestHandler(function (ClientRequest $request) {
$length = $request->getRequestHeaders()['content-length'][0] ?? null;
$body = new RequestBody($request, null === $length ? null : (int) $length); // a PSR-7 stream
$lines = 0;
while ('' !== ($bytes = $body->read(8192))) { // waits for the next bytes; '' only at the end
$lines += substr_count($bytes, "\n");
}
$request->write("received {$body->getSize()} bytes, $lines lines\n");
});
if (debug_backtrace()) {
return $app; // swerve loads this file: it is the application's swerve.php
}
// Run directly: serve the application with swerve, and send it requests
require __DIR__ . '/../serve.php';
$csv = "id,name\n1,ann\n2,bob\n3,cy\n";
serve(__FILE__, ["POST /import HTTP/1.1\r\nHost: example\r\nContent-Length: " . strlen($csv) . "\r\n\r\n$csv"]);Output
HTTP/1.1 200 OK Server: Swerve Transfer-Encoding: chunked Connection: close 1b received 25 bytes, 4 lines 0
RequestHandler#
use Swerve\ClientRequest;
use Swerve\RequestHandler;
// swerve.php returns the handler; swerve calls it once for each request
$app = new RequestHandler(function (ClientRequest $request) {
$request->sendResponseHeaders(200, ['content-type' => 'text/plain', 'content-length' => '13']);
$request->write("Hello, world\n");
}); // returning ends the response; the connection serves its next request
if (debug_backtrace()) {
return $app; // swerve loads this file: it is the application's swerve.php
}
// Run directly: serve the application with swerve, and send it requests
require __DIR__ . '/../serve.php';
serve(__FILE__, ["GET / HTTP/1.1\r\nHost: example\r\n\r\n"]);Output
HTTP/1.1 200 OK content-type: text/plain Server: Swerve Content-Length: 13 Connection: close Hello, world
RequestHandler#
// swerve.php
return new Swerve\RequestHandler(function (Swerve\ClientRequest $request) {
$request->write("Hello, world\n");
});ServerInterface#
$server->listen(); // throws when the address can't be bound
phasync::run($server->run(...)); // returns once drain() was called and every connection endedStaticFiles#
use Swerve\ClientRequest;
use Swerve\RequestHandler;
use Swerve\StaticFiles;
// Files below public/ are served; every other request reaches the application
$files = new StaticFiles(__DIR__ . '/public');
$app = new RequestHandler($files->wrap(function (ClientRequest $request) {
$request->sendResponseHeaders(404, ['content-type' => 'text/plain']);
$request->write("No page at {$request->getTarget()}\n");
}));
if (debug_backtrace()) {
return $app; // swerve loads this file: it is the application's swerve.php
}
// Run directly: serve the application with swerve, and send it requests
require __DIR__ . '/../serve.php';
touch(__DIR__ . '/public/hello.txt', 1767225600); // a fixed date, for the same output every run
serve(__FILE__, [
"GET /hello.txt HTTP/1.1\r\nHost: example\r\n\r\n",
"GET /missing.txt HTTP/1.1\r\nHost: example\r\n\r\n",
]);Output
HTTP/1.1 200 OK Content-Type: text/plain; charset=utf-8 Last-Modified: Thu, 01 Jan 2026 00:00:00 GMT ETag: "6955b900-12" Accept-Ranges: bytes Server: Swerve Content-Length: 18 Connection: close Hello from a file HTTP/1.1 404 Not Found content-type: text/plain Server: Swerve Transfer-Encoding: chunked Connection: close 18 No page at /missing.txt 0
StaticFiles#
$files = new Swerve\StaticFiles(__DIR__ . '/public');
return new Swerve\RequestHandler($files->wrap(function (Swerve\ClientRequest $request) {
// what is not a file
}));StaticFiles::__construct#
$files = new StaticFiles(__DIR__ . '/public');StaticFiles::wrap#
use Swerve\ClientRequest;
use Swerve\RequestHandler;
use Swerve\StaticFiles;
$app = new RequestHandler((new StaticFiles(__DIR__ . '/../StaticFiles/public'))->wrap(function (ClientRequest $request) {
$request->sendResponseHeaders(404);
$request->end();
}));
if (debug_backtrace()) {
return $app; // swerve loads this file: it is the application's swerve.php
}
// Run directly: serve the application with swerve, and send it requests
require __DIR__ . '/../serve.php';
touch(__DIR__ . '/../StaticFiles/public/hello.txt', 1767225600); // a fixed date, for the same output every run
serve(__FILE__, [
"GET /hello.txt HTTP/1.1\r\nHost: example\r\nRange: bytes=0-4\r\n\r\n", // 206, the first 5 bytes
"GET /hello.txt HTTP/1.1\r\nHost: example\r\nIf-None-Match: \"6955b900-12\"\r\n\r\n", // 304, the client has it
"GET /serve.php HTTP/1.1\r\nHost: example\r\n\r\n", // never a PHP source
]);Output
HTTP/1.1 206 Partial Content Content-Range: bytes 0-4/18 Content-Type: text/plain; charset=utf-8 Last-Modified: Thu, 01 Jan 2026 00:00:00 GMT ETag: "6955b900-12" Accept-Ranges: bytes Server: Swerve Content-Length: 5 Connection: close Hello HTTP/1.1 304 Not Modified Content-Type: text/plain; charset=utf-8 Last-Modified: Thu, 01 Jan 2026 00:00:00 GMT ETag: "6955b900-12" Accept-Ranges: bytes Server: Swerve Connection: close HTTP/1.1 404 Not Found Server: Swerve Content-Length: 0 Connection: close
Swerve#
use Swerve\Swerve;
// Without swerve's master process (a script, a test), Swerve works within this process
phasync::run(function () {
$chat = Swerve::subscribe('chat'); // subscribed from here on
Swerve::publish('chat', ['user' => 'ann', 'text' => 'hi']);
foreach ($chat as $message) {
echo "$message->user: $message->text\n";
break;
}
Swerve::cache()->set('visits', 1, 60);
echo 'visits: ', Swerve::cache()->get('visits'), "\n";
});Output
ann: hi visits: 1
Swerve#
use Swerve\Swerve;
Swerve::log()->info('started');
Swerve::cache()->set('hits', 1, 60);
Swerve::publish('chat', ['user' => 'ann', 'text' => 'hi']);