phasync
Request handlers / Examples

Request handlers

Examples

Every example of this topic, with its output.

ClientRequest#

Example 1Read the request, write the response
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#

Example 1
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#

Example 1An interim 103 and a final 201
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}
0

ClientRequest::flush#

Example 1Put the head on the wire before the work
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#

Example 1Send part of a file
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#

Example 1Trailers after a chunked body
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#

Example 1A status after the body began
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#

Example 1The fields of a url-encoded form
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#

Example 1Which requests have a form
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#

Example 1An uploaded file
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#

Example 1The raw bytes after parsing
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 parsing
Output
array (
  'a' => '1',
  'b' => '2',
)
a=1&b=2

RequestBody#

Example 1Read a body as it arrives
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#

Example 1Hello, world
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#

Example 1
// swerve.php
return new Swerve\RequestHandler(function (Swerve\ClientRequest $request) {
    $request->write("Hello, world\n");
});

ServerInterface#

Example 1
$server->listen();                    // throws when the address can't be bound
phasync::run($server->run(...));      // returns once drain() was called and every connection ended

StaticFiles#

Example 1Files, and the application for the rest
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#

Example 1
$files = new Swerve\StaticFiles(__DIR__ . '/public');
return new Swerve\RequestHandler($files->wrap(function (Swerve\ClientRequest $request) {
    // what is not a file
}));

StaticFiles::__construct#

Example 1
$files = new StaticFiles(__DIR__ . '/public');

StaticFiles::wrap#

Example 1Ranges, revalidation, and no PHP sources
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#

Example 1Messages and the cache within one process
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#

Example 1
use Swerve\Swerve;

Swerve::log()->info('started');
Swerve::cache()->set('hits', 1, 60);
Swerve::publish('chat', ['user' => 'ann', 'text' => 'hi']);