Repository files navigation

Signalforge HTTP Extension

CIPHP 8.3+

A native PHP extension implementing high-performance PSR-7 HTTP Request and Response classes with zero-copy operations and direct superglobal access.

What's Different

  • Native C implementation - all HTTP operations run in native code
  • Zero-copy string streams - reference strings directly without data duplication
  • Direct HashTable access - bypass PHP arrays for superglobal data
  • Lazy evaluation - parse data only when accessed
  • Immutable objects - all with*() methods return new instances
  • Memory efficient - proper reference counting and cleanup
  • PSR-7 compliant - implements ServerRequestInterface, ResponseInterface, StreamInterface, UriInterface, and UploadedFileInterface
  • Optimized for php-fpm - designed for FastCGI environments
  • No dependencies - pure C extension with no external libraries

Why C?

HTTP request/response handling is invoked on nearly every request, often hundreds of times. Moving HTTP operations to native code provides:

  • Direct superglobal access - bypass PHP's array layer for $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
  • Zero-copy string operations - reference string data directly without duplication
  • Native hash tables - efficient storage and lookup for headers and parameters
  • Reduced overhead - minimal PHP engine interaction during data access
  • Memory efficiency - proper reference counting and cleanup
  • Lazy evaluation - parse JSON/form data only when requested
  • Immutable operations - efficient object cloning with shared data structures

Features

  • Full PSR-7 Compliance: Implements ServerRequestInterface, ResponseInterface, StreamInterface, UriInterface, and UploadedFileInterface
  • Zero Dependencies: Pure C extension with no external libraries
  • Hyper-Performance: Direct HashTable access, zero-copy operations, lazy evaluation
  • Immutable Objects: All with*() methods return new instances
  • Memory Efficient: Proper reference counting and cleanup

Streamforge Proxy Integration

The extension integrates seamlessly with the Streamforge FastCGI proxy for high-performance file upload handling. When Streamforge is deployed between nginx and php-fpm, it provides several benefits depending on your nginx configuration.

Understanding nginx Buffering

Important: nginx's fastcgi_request_buffering setting affects what problem streamforge solves:

WITH fastcgi_request_buffering ON (nginx default):
┌────────┐ ┌───────────────┐ ┌─────────┐ ┌───────────┐
│ Client │────▶│ nginx buffers │────▶│ php-fpm │────▶│ $_FILES │
└────────┘ └───────────────┘ └─────────┘ └───────────┘
slow fast │
(to disk) Worker engaged only
during fast transfer
WITH fastcgi_request_buffering OFF:
┌────────┐ ┌───────┐ ┌─────────┐
│ Client │────▶│ nginx │────▶│ php-fpm │ ← Worker blocked for entire upload!
└────────┘ └───────┘ └─────────┘
slow streams
directly

With default nginx settings, workers are already protected from slow clients. The upload is buffered by nginx first.

When Streamforge Helps

nginx settingStreamforge benefit
fastcgi_request_buffering on (default)Avoids double temp file write, reduces PHP memory, skips multipart parsing in PHP
fastcgi_request_buffering offFull benefit: Workers not blocked during slow uploads

With fastcgi_request_buffering off

This is where streamforge shines. Configure nginx to stream directly:

location /upload {fastcgi_request_buffering off; # Stream to backend
fastcgi_pass streamforge:9001;}

Now streamforge handles the slow client I/O:

┌────────┐ ┌─────────────┐ ┌─────────┐
│ Client │────▶│ streamforge │────▶│ php-fpm │
└────────┘ └─────────────┘ └─────────┘
slow writes to disk Worker engaged
as data arrives only ~5ms
ScenarioWithout StreamforgeWith Streamforge
500MB upload, slow clientWorker blocked ~30sWorker engaged ~5ms
20 concurrent uploads, 10 workersSite unresponsiveNo impact

With Default nginx (buffering on)

Streamforge still provides value, just different:

  • No double write: nginx buffer → streamforge disk (not nginx buffer → PHP temp)
  • Less PHP memory: No request body buffering in PHP
  • No multipart parsing: PHP doesn't parse multipart boundaries
  • Consistent API: Same HTTP_X_UPLOAD_* interface regardless of nginx config

Transparent Integration

The extension automatically detects Streamforge and reads uploads from the appropriate source. Your application code remains unchanged:

// Works identically with or without Streamforge$request = Request::capture();
$files = $request->getUploadedFiles();
foreach ($filesas$name => $file) {
$file->getClientFilename(); // "document.pdf"$file->getSize(); // 52428800$file->moveTo('/storage/docs/document.pdf');
}

Detection API

Check if Streamforge is handling the current request:

useSignalforge\NativeHttp\Request;
// Static methodif (Request::isStreamforgeEnabled()) {
// Streamforge is proxying this request
}
// Or check $_SERVER directlyif (isset($_SERVER['HTTP_X_STREAMFORGE'])) {
// Streamforge marker present
}
// Check for processed uploadsif (isset($_SERVER['HTTP_X_UPLOAD_FILE_COUNT'])) {
$count = (int) $_SERVER['HTTP_X_UPLOAD_FILE_COUNT'];
// Streamforge handled $count file uploads
}

Protocol

When Streamforge handles multipart uploads, it:

  1. Parses the multipart body and writes files to disk
  2. Adds metadata headers to the FastCGI request:
    • HTTP_X_STREAMFORGE=1 - Proxy marker
    • HTTP_X_UPLOAD_FILE_COUNT=N - Number of uploaded files
    • HTTP_X_UPLOAD_0_NAME - Form field name
    • HTTP_X_UPLOAD_0_FILENAME - Original client filename
    • HTTP_X_UPLOAD_0_PATH - Temp file path on disk
    • HTTP_X_UPLOAD_0_SIZE - File size in bytes
    • HTTP_X_UPLOAD_0_TYPE - MIME type
  3. Sends only form fields (not file content) to PHP-FPM

The extension reads these headers and creates UploadedFile objects that work identically to standard PHP uploads.

Cleanup

Temp files are automatically cleaned up:

  • On moveTo(): File is moved, no cleanup needed
  • On request end: Unmoved temp files are deleted by the extension's RSHUTDOWN handler

This prevents disk space leaks even if application code doesn't handle all uploaded files.

Deployment

See the Streamforge documentation for deployment instructions. Basic setup:

# Start Streamforge between nginx and php-fpm
streamforge -l 0.0.0.0:9001 -u /var/run/php-fpm.sock -d /tmp/uploads
# Configure nginx to send requests to Streamforge# fastcgi_pass 127.0.0.1:9001;

Requirements

  • PHP 8.3, 8.4, or 8.5
  • Linux or macOS (tested on x86_64 and ARM64)
  • php-fpm recommended (works in CLI for testing)

Building

Docker (Recommended)

No need to install PHP dev headers on your host:

cd http
# Build Docker image with extension
make docker-build
# Run tests
make docker-test
# Test all PHP versions (8.3, 8.4, 8.5)
make ci-test-all
# Run example
make docker-example

Host Installation

cd http
phpize
./configure --enable-signalforge_http
make
make test
sudo make install

Then add extension=signalforge_http.so to your php.ini.

Usage

Request

<?phpuseSignalforge\NativeHttp\Request;
// Capture the current request$request = Request::capture();
// HTTP Method & URI$method = $request->getMethod(); // "POST"$target = $request->getRequestTarget(); // "/users/123?include=profile"$path = $request->getUri(); // "/users/123?include=profile"// Headers (case-insensitive)$contentType = $request->getHeader('Content-Type'); // ['application/json']$contentTypeLine = $request->getHeaderLine('Content-Type'); // "application/json"$hasAuth = $request->hasHeader('Authorization'); // true/false$allHeaders = $request->getHeaders(); // ['content-type' => ['application/json']]// Parameters$queryParams = $request->getQueryParams(); // $_GET as array$parsedBody = $request->getParsedBody(); // JSON/form data (lazy parsed)// Body access$bodyStream = $request->getBody(); // StreamInterface$rawBody = (string) $request->getBody(); // Raw body string// Server & environment$serverParams = $request->getServerParams(); // $_SERVER$userAgent = $serverParams['HTTP_USER_AGENT'];
// Cookies$cookies = $request->getCookieParams(); // $_COOKIE as array$sessionId = $cookies['session_id'];
// Uploaded files$files = $request->getUploadedFiles(); // Normalized file structureif (isset($files['avatar'])) {
$filename = $files['avatar']->getClientFilename();
$files['avatar']->moveTo('/uploads/' . $filename);
}
// Attributes (middleware data)$request = $request->withAttribute('user_id', 123);
$userId = $request->getAttribute('user_id'); // 123$userId = $request->getAttribute('missing', 'default'); // 'default'// Immutable modifications$newRequest = $request
->withMethod('PUT')
->withHeader('X-API-Key', 'secret')
->withQueryParams(['limit' => 10])
->withParsedBody(['name' => 'John']);
// Original request unchangedassert($request->getMethod() === 'POST');
assert($newRequest->getMethod() === 'PUT');

Response

<?phpuseSignalforge\NativeHttp\Response;
useSignalforge\NativeHttp\Stream;
// Factory methods$response = Response::create(200, ['Content-Type' => 'application/json']);
$response = Response::json(['users' => ['id' => 1, 'name' => 'John']], 200);
$response = Response::text('Hello World', 200);
$response = Response::html('<h1>Welcome</h1>', 200);
$response = Response::redirect('/login', 302);
// Status management$statusCode = $response->getStatusCode(); // 200$reasonPhrase = $response->getReasonPhrase(); // "OK"$response = $response->withStatus(404, 'Not Found');
// Header management (case-insensitive)$response = $response->withHeader('Content-Type', 'application/json');
$response = $response->withAddedHeader('Cache-Control', 'no-cache');
$response = $response->withAddedHeader('Cache-Control', 'private');
$hasHeader = $response->hasHeader('Content-Type'); // true$headerValue = $response->getHeader('Content-Type'); // ['application/json']$headerLine = $response->getHeaderLine('Content-Type'); // "application/json"$allHeaders = $response->getHeaders();
// Body management$stream = Stream::fromString('{"message": "Hello"}');
$response = $response->withBody($stream);
$bodyStream = $response->getBody();
// Output$response->send(); // Send headers + body$response->sendHeaders(); // Send only headers$response->sendBody(); // Send only body// Serialization$message = (string) $response; // Full HTTP message

Stream

<?phpuseSignalforge\NativeHttp\Stream;
// Factory methods$stream = Stream::fromString('Hello World'); // TRUE zero-copy string reference$stream = Stream::fromResource(fopen('file.txt', 'r')); // From PHP resource$stream = Stream::fromFile('/path/to/file', 'r'); // From file path// Reading operations$data = $stream->read(5); // Read 5 bytes: "Hello"$remaining = $stream->getContents(); // Get rest: " World"$stream->rewind(); // Reset to beginning$all = (string) $stream; // Get entire contents// Writing operations (use file or resource streams for writing)$writableStream = Stream::fromFile('/tmp/output.txt', 'w+');
$bytesWritten = $writableStream->write('Hello'); // Write data$writableStream->write(' World'); // Append more// Seeking operations$stream->seek(6); // Seek to position 6$position = $stream->tell(); // Get current position: 6$stream->rewind(); // Reset to beginning// Stream capabilities$isReadable = $stream->isReadable(); // Check if can read$isWritable = $stream->isWritable(); // Check if can write$isSeekable = $stream->isSeekable(); // Check if supports seeking$atEnd = $stream->eof(); // Check if at end// Metadata and size$size = $stream->getSize(); // Size in bytes (or null)$metadata = $stream->getMetadata(); // All metadata$uri = $stream->getMetadata('uri'); // Specific metadata key// Resource management$underlying = $stream->detach(); // Detach PHP resource$stream->close(); // Close stream and free resources

Uri

<?phpuseSignalforge\NativeHttp\Uri;
// Parse a URI string$uri = Uri::fromString('https://user:pass@example.com:8080/path?query=value#fragment');
// Access components (PSR-7 UriInterface)$scheme = $uri->getScheme(); // "https"$userInfo = $uri->getUserInfo(); // "user:pass"$host = $uri->getHost(); // "example.com"$port = $uri->getPort(); // 8080 (null if standard port for scheme)$path = $uri->getPath(); // "/path"$query = $uri->getQuery(); // "query=value"$fragment = $uri->getFragment(); // "fragment"$authority = $uri->getAuthority(); // "user:pass@example.com:8080"// Serialize to string$uriString = (string) $uri; // "https://user:pass@example.com:8080/path?query=value#fragment"// Immutable modifications$newUri = $uri
->withScheme('http')
->withHost('api.example.com')
->withPort(null) // Remove explicit port
->withPath('/v2/users')
->withQuery('limit=10')
->withFragment('');
// Original URI unchangedassert($uri->getHost() === 'example.com');
assert($newUri->getHost() === 'api.example.com');
// Standard ports are normalized to null$httpsUri = Uri::fromString('https://example.com:443/path');
$port = $httpsUri->getPort(); // null (443 is standard for https)

UploadedFile

<?phpuseSignalforge\NativeHttp\Request;
// Get uploaded files from request$request = Request::capture();
$files = $request->getUploadedFiles();
// Single file uploadif (isset($files['avatar'])) {
$file = $files['avatar'];
// File properties$size = $file->getSize(); // Size in bytes$error = $file->getError(); // UPLOAD_ERR_* constant$clientName = $file->getClientFilename(); // Original filename$mimeType = $file->getClientMediaType(); // MIME type// Move file to permanent location$targetPath = '/uploads/avatars/' . uniqid() . '_' . $clientName;
$file->moveTo($targetPath);
// Note: moveTo() can only be called once per UploadedFile
}
// Multiple file uploadif (isset($files['photos'])) {
foreach ($files['photos'] as$photo) {
if ($photo->getError() === UPLOAD_ERR_OK) {
$filename = $photo->getClientFilename();
$photo->moveTo('/uploads/photos/' . $filename);
}
}
}
// Stream access (alternative to moveTo)$stream = $file->getStream();
$content = $stream->getContents();

Advanced Patterns

<?phpuseSignalforge\NativeHttp\{Request, Response, Stream};
// Middleware-style request processingfunctionauthenticate(Request$request): Request
{
$token = $request->getHeaderLine('Authorization');
$userId = validateToken($token);
return$request->withAttribute('user_id', $userId);
}
functionvalidateJson(Request$request): Request
{
$contentType = $request->getHeaderLine('Content-Type');
if (!str_contains($contentType, 'application/json')) {
thrownewInvalidArgumentException('JSON content type required');
}
return$request;
}
// Request processing pipeline$request = Request::capture();
$request = authenticate($request);
$request = validateJson($request);
// JSON API response$data = ['users' => getUsers($request->getAttribute('user_id'))];
$response = Response::json($data, 200);
// CORS headers$response = $response
->withHeader('Access-Control-Allow-Origin', '*')
->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// Conditional responseif ($request->hasHeader('If-None-Match')) {
$etag = $request->getHeaderLine('If-None-Match');
if ($etag === generateEtag($data)) {
$response = $response->withStatus(304); // Not Modified
}
}
$response->send();

API Reference

Request

Factory Methods

Request::capture(): ServerRequestInterface // Capture current request from superglobals

PSR-7 MessageInterface Methods

getProtocolVersion(): string // Get HTTP protocol version (always "1.1" in FastCGI)withProtocolVersion(string $version): static // Return new instance with protocol versiongetHeaders(): array // Get all headers as lowercase key => array valueshasHeader(string $name): bool // Check if header exists (case-insensitive)getHeader(string $name): array // Get header values arraygetHeaderLine(string $name): string // Get header values as comma-separated stringwithHeader(string $name, string|array $value): static // Replace header (case-insensitive)withAddedHeader(string $name, string|array $value): static // Add to existing headerwithoutHeader(string $name): static // Remove headergetBody(): StreamInterface // Get message body streamwithBody(StreamInterface $body): static // Replace body stream

PSR-7 RequestInterface Methods

getRequestTarget(): string // Get request target (path + query)withRequestTarget(string $target): static // Set request targetgetMethod(): string // Get HTTP methodwithMethod(string $method): static // Set HTTP methodgetUri(): string // Get URI as stringwithUri(string|UriInterface $uri, bool $preserveHost = false): static // Set URI

PSR-7 ServerRequestInterface Methods

getServerParams(): array // Get $_SERVER parametersgetCookieParams(): array // Get $_COOKIE parameterswithCookieParams(array $cookies): static // Replace cookiesgetQueryParams(): array // Get $_GET parameterswithQueryParams(array $query): static // Replace query parametersgetUploadedFiles(): array // Get uploaded files structurewithUploadedFiles(array $files): static // Replace uploaded filesgetParsedBody(): array|object|null // Get parsed body (JSON/form data)withParsedBody(array|object|null $data): static // Set parsed bodygetAttributes(): array // Get request attributesgetAttribute(string $name, mixed $default = null) // Get single attributewithAttribute(string $name, mixed $value): static // Add/replace attributewithoutAttribute(string $name): static // Remove attribute

Response

Factory Methods

Response::create(int $status = 200, array $headers = [], mixed $body = null): static
Response::json(mixed $data, int $status = 200): static
Response::text(string $text, int $status = 200): static
Response::html(string $html, int $status = 200): static
Response::redirect(string $url, int $status = 302): static

PSR-7 MessageInterface Methods

getProtocolVersion(): string // Get HTTP protocol versionwithProtocolVersion(string $version): static // Set protocol versiongetHeaders(): array // Get all headershasHeader(string $name): bool // Check header existsgetHeader(string $name): array // Get header valuesgetHeaderLine(string $name): string // Get comma-separated headerwithHeader(string $name, string|array $value): static // Replace headerwithAddedHeader(string $name, string|array $value): static // Add header valuewithoutHeader(string $name): static // Remove headergetBody(): StreamInterface // Get body streamwithBody(StreamInterface $body): static // Replace body stream

PSR-7 ResponseInterface Methods

getStatusCode(): int // Get HTTP status codewithStatus(int $code, string $reason = ''): static // Set status code and reasongetReasonPhrase(): string // Get reason phrase

Output Methods

send(): void // Send response (headers + body)sendHeaders(): void // Send only headerssendBody(): void // Send only body__toString(): string // Serialize to HTTP message

Stream

Factory Methods

Stream::fromString(string $string): static // Create from string (zero-copy)
Stream::fromResource(resource $resource): static // Create from PHP stream resource
Stream::fromFile(string $path, string $mode = 'r'): static // Create from file

PSR-7 StreamInterface Methods

read(int $length): string // Read data from stream
getContents(): string // Get remaining contentswrite(string $string): int // Write data to streamseek(int $offset, int $whence = SEEK_SET): void // Seek to positiontell(): int // Get current positionrewind(): void // Seek to beginningeof(): bool // Check if at end of streamisReadable(): bool // Check if stream is readableisWritable(): bool // Check if stream is writableisSeekable(): bool // Check if stream supports seekinggetSize(): ?int // Get stream size (if known)getMetadata(?string $key = null): mixed // Get stream metadataclose(): void // Close stream and free resourcesdetach(): resource|null // Detach underlying resource__toString(): string // Get entire stream contents

UploadedFile

PSR-7 UploadedFileInterface Methods

getStream(): StreamInterface // Get file contents as streammoveTo(string $targetPath): void // Move file to new locationgetSize(): ?int // Get file size in bytesgetError(): int // Get upload error code (UPLOAD_ERR_*)getClientFilename(): ?string // Get original client filenamegetClientMediaType(): ?string // Get client-provided MIME type

Uri

Factory Methods

Uri::fromString(string $uri): UriInterface // Parse URI string (RFC 3986 compliant)

PSR-7 UriInterface Methods

getScheme(): string // Get URI scheme (http, https, etc.)getAuthority(): string // Get authority (userinfo@host:port)getUserInfo(): string // Get user info (user:pass)getHost(): string // Get host (lowercase)getPort(): ?int // Get port (null if standard for scheme)getPath(): string // Get path componentgetQuery(): string // Get query string (without ?)getFragment(): string // Get fragment (without #)withScheme(string $scheme): UriInterface // Return new instance with schemewithUserInfo(string $user, ?string $pass = null): UriInterface // Set user infowithHost(string $host): UriInterface // Set hostwithPort(?int $port): UriInterface // Set port (null to remove)withPath(string $path): UriInterface // Set pathwithQuery(string $query): UriInterface // Set query stringwithFragment(string $fragment): UriInterface // Set fragment__toString(): string // Serialize to URI string

Performance

The extension provides significant performance improvements over userland PSR-7 implementations through native C code, direct superglobal access, and zero-copy operations. Benchmarks comparing against other PSR-7 implementations can be found in the http-php repository.

Key Optimizations

  • Direct superglobal access - bypasses PHP's array layer for $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
  • Zero-copy string streams - reference strings directly without data duplication
  • Native hash tables - efficient storage and lookup for headers and parameters
  • Lazy evaluation - parse JSON/form data only when accessed
  • Immutable operations - efficient object cloning with shared data structures
  • Memory efficient - proper reference counting and cleanup

How It Works

Request Capture Process

  1. Direct superglobal access - References $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES directly
  2. Lazy header parsing - Headers parsed only when getHeaders() is called
  3. JSON caching - Parsed JSON bodies cached to avoid re-parsing
  4. Immutable cloning - with*() methods create efficient clones with shared data

Stream Operations

  • String streams: TRUE zero-copy references to existing strings (no data duplication)
  • Resource streams: Efficient php_stream_copy_to_mem() for large data
  • Lazy loading: Stream contents read only when accessed
  • Position tracking: Efficient position management for seekable streams

Memory Management

  • Reference counting: Proper Zend reference counting throughout
  • Object pooling: Reuses memory structures where possible
  • Automatic cleanup: Destructors handle resource cleanup
  • Leak prevention: All allocations properly tracked and freed

Structure

http/
├── config.m4 # Build configuration
├── signalforge_http.c # PHP class implementations
├── php_signalforge_http.h # Main header
├── src/
│ ├── request.c/h # Request class implementation
│ ├── response.c/h # Response class implementation
│ ├── stream.c/h # Stream class implementation
│ ├── uri.c/h # Uri class implementation
│ ├── uploadedfile.c/h # UploadedFile class implementation
│ ├── psr7_interfaces.c/h # PSR-7 interface definitions
├── Signalforge/Http/ # IDE stubs
├── examples/ # Usage examples
├── tests/ # phpt test files (97 tests)
└── Dockerfile # Docker build environment

Testing

make test

Or run specific tests:

docker run --rm signalforge-http php /opt/run-tests.php tests/001_basic.phpt

Memory Leak Detection

# Docker-based Valgrind (recommended)
make valgrind-docker
# Local Valgrind (requires valgrind installed)
make valgrind-test

Thread Safety

The extension supports ZTS (Zend Thread Safety) builds. Each request gets isolated instances, and all operations are thread-safe.

Exception Handling

  • InvalidArgumentException - Invalid parameters or malformed data
  • RuntimeException - Stream operations, file access errors
  • Standard PHP exceptions for JSON parsing errors

Related

License

MIT License

About

A native PHP extension implementing a high-performance PSR-7, PSR-17 and PSR-18 compliant HTTP request and response handling - direct HashTable access, zero-copy strings, and lazy evaluation.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Signalforge HTTP Extension

CIPHP 8.3+

A native PHP extension implementing high-performance PSR-7 HTTP Request and Response classes with zero-copy operations and direct superglobal access.

What's Different

  • Native C implementation - all HTTP operations run in native code
  • Zero-copy string streams - reference strings directly without data duplication
  • Direct HashTable access - bypass PHP arrays for superglobal data
  • Lazy evaluation - parse data only when accessed
  • Immutable objects - all with*() methods return new instances
  • Memory efficient - proper reference counting and cleanup
  • PSR-7 compliant - implements ServerRequestInterface, ResponseInterface, StreamInterface, UriInterface, and UploadedFileInterface
  • Optimized for php-fpm - designed for FastCGI environments
  • No dependencies - pure C extension with no external libraries

Why C?

HTTP request/response handling is invoked on nearly every request, often hundreds of times. Moving HTTP operations to native code provides:

  • Direct superglobal access - bypass PHP's array layer for $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
  • Zero-copy string operations - reference string data directly without duplication
  • Native hash tables - efficient storage and lookup for headers and parameters
  • Reduced overhead - minimal PHP engine interaction during data access
  • Memory efficiency - proper reference counting and cleanup
  • Lazy evaluation - parse JSON/form data only when requested
  • Immutable operations - efficient object cloning with shared data structures

Features

  • Full PSR-7 Compliance: Implements ServerRequestInterface, ResponseInterface, StreamInterface, UriInterface, and UploadedFileInterface
  • Zero Dependencies: Pure C extension with no external libraries
  • Hyper-Performance: Direct HashTable access, zero-copy operations, lazy evaluation
  • Immutable Objects: All with*() methods return new instances
  • Memory Efficient: Proper reference counting and cleanup

Streamforge Proxy Integration

The extension integrates seamlessly with the Streamforge FastCGI proxy for high-performance file upload handling. When Streamforge is deployed between nginx and php-fpm, it provides several benefits depending on your nginx configuration.

Understanding nginx Buffering

Important: nginx's fastcgi_request_buffering setting affects what problem streamforge solves:

WITH fastcgi_request_buffering ON (nginx default):
┌────────┐ ┌───────────────┐ ┌─────────┐ ┌───────────┐
│ Client │────▶│ nginx buffers │────▶│ php-fpm │────▶│ $_FILES │
└────────┘ └───────────────┘ └─────────┘ └───────────┘
slow fast │
(to disk) Worker engaged only
during fast transfer
WITH fastcgi_request_buffering OFF:
┌────────┐ ┌───────┐ ┌─────────┐
│ Client │────▶│ nginx │────▶│ php-fpm │ ← Worker blocked for entire upload!
└────────┘ └───────┘ └─────────┘
slow streams
directly

With default nginx settings, workers are already protected from slow clients. The upload is buffered by nginx first.

When Streamforge Helps

nginx settingStreamforge benefit
fastcgi_request_buffering on (default)Avoids double temp file write, reduces PHP memory, skips multipart parsing in PHP
fastcgi_request_buffering offFull benefit: Workers not blocked during slow uploads

With fastcgi_request_buffering off

This is where streamforge shines. Configure nginx to stream directly:

location /upload {fastcgi_request_buffering off; # Stream to backend
fastcgi_pass streamforge:9001;}

Now streamforge handles the slow client I/O:

┌────────┐ ┌─────────────┐ ┌─────────┐
│ Client │────▶│ streamforge │────▶│ php-fpm │
└────────┘ └─────────────┘ └─────────┘
slow writes to disk Worker engaged
as data arrives only ~5ms
ScenarioWithout StreamforgeWith Streamforge
500MB upload, slow clientWorker blocked ~30sWorker engaged ~5ms
20 concurrent uploads, 10 workersSite unresponsiveNo impact

With Default nginx (buffering on)

Streamforge still provides value, just different:

  • No double write: nginx buffer → streamforge disk (not nginx buffer → PHP temp)
  • Less PHP memory: No request body buffering in PHP
  • No multipart parsing: PHP doesn't parse multipart boundaries
  • Consistent API: Same HTTP_X_UPLOAD_* interface regardless of nginx config

Transparent Integration

The extension automatically detects Streamforge and reads uploads from the appropriate source. Your application code remains unchanged:

// Works identically with or without Streamforge$request = Request::capture();
$files = $request->getUploadedFiles();
foreach ($filesas$name => $file) {
$file->getClientFilename(); // "document.pdf"$file->getSize(); // 52428800$file->moveTo('/storage/docs/document.pdf');
}

Detection API

Check if Streamforge is handling the current request:

useSignalforge\NativeHttp\Request;
// Static methodif (Request::isStreamforgeEnabled()) {
// Streamforge is proxying this request
}
// Or check $_SERVER directlyif (isset($_SERVER['HTTP_X_STREAMFORGE'])) {
// Streamforge marker present
}
// Check for processed uploadsif (isset($_SERVER['HTTP_X_UPLOAD_FILE_COUNT'])) {
$count = (int) $_SERVER['HTTP_X_UPLOAD_FILE_COUNT'];
// Streamforge handled $count file uploads
}

Protocol

When Streamforge handles multipart uploads, it:

  1. Parses the multipart body and writes files to disk
  2. Adds metadata headers to the FastCGI request:
    • HTTP_X_STREAMFORGE=1 - Proxy marker
    • HTTP_X_UPLOAD_FILE_COUNT=N - Number of uploaded files
    • HTTP_X_UPLOAD_0_NAME - Form field name
    • HTTP_X_UPLOAD_0_FILENAME - Original client filename
    • HTTP_X_UPLOAD_0_PATH - Temp file path on disk
    • HTTP_X_UPLOAD_0_SIZE - File size in bytes
    • HTTP_X_UPLOAD_0_TYPE - MIME type
  3. Sends only form fields (not file content) to PHP-FPM

The extension reads these headers and creates UploadedFile objects that work identically to standard PHP uploads.

Cleanup

Temp files are automatically cleaned up:

  • On moveTo(): File is moved, no cleanup needed
  • On request end: Unmoved temp files are deleted by the extension's RSHUTDOWN handler

This prevents disk space leaks even if application code doesn't handle all uploaded files.

Deployment

See the Streamforge documentation for deployment instructions. Basic setup:

# Start Streamforge between nginx and php-fpm
streamforge -l 0.0.0.0:9001 -u /var/run/php-fpm.sock -d /tmp/uploads
# Configure nginx to send requests to Streamforge# fastcgi_pass 127.0.0.1:9001;

Requirements

  • PHP 8.3, 8.4, or 8.5
  • Linux or macOS (tested on x86_64 and ARM64)
  • php-fpm recommended (works in CLI for testing)

Building

Docker (Recommended)

No need to install PHP dev headers on your host:

cd http
# Build Docker image with extension
make docker-build
# Run tests
make docker-test
# Test all PHP versions (8.3, 8.4, 8.5)
make ci-test-all
# Run example
make docker-example

Host Installation

cd http
phpize
./configure --enable-signalforge_http
make
make test
sudo make install

Then add extension=signalforge_http.so to your php.ini.

Usage

Request

<?phpuseSignalforge\NativeHttp\Request;
// Capture the current request$request = Request::capture();
// HTTP Method & URI$method = $request->getMethod(); // "POST"$target = $request->getRequestTarget(); // "/users/123?include=profile"$path = $request->getUri(); // "/users/123?include=profile"// Headers (case-insensitive)$contentType = $request->getHeader('Content-Type'); // ['application/json']$contentTypeLine = $request->getHeaderLine('Content-Type'); // "application/json"$hasAuth = $request->hasHeader('Authorization'); // true/false$allHeaders = $request->getHeaders(); // ['content-type' => ['application/json']]// Parameters$queryParams = $request->getQueryParams(); // $_GET as array$parsedBody = $request->getParsedBody(); // JSON/form data (lazy parsed)// Body access$bodyStream = $request->getBody(); // StreamInterface$rawBody = (string) $request->getBody(); // Raw body string// Server & environment$serverParams = $request->getServerParams(); // $_SERVER$userAgent = $serverParams['HTTP_USER_AGENT'];
// Cookies$cookies = $request->getCookieParams(); // $_COOKIE as array$sessionId = $cookies['session_id'];
// Uploaded files$files = $request->getUploadedFiles(); // Normalized file structureif (isset($files['avatar'])) {
$filename = $files['avatar']->getClientFilename();
$files['avatar']->moveTo('/uploads/' . $filename);
}
// Attributes (middleware data)$request = $request->withAttribute('user_id', 123);
$userId = $request->getAttribute('user_id'); // 123$userId = $request->getAttribute('missing', 'default'); // 'default'// Immutable modifications$newRequest = $request
->withMethod('PUT')
->withHeader('X-API-Key', 'secret')
->withQueryParams(['limit' => 10])
->withParsedBody(['name' => 'John']);
// Original request unchangedassert($request->getMethod() === 'POST');
assert($newRequest->getMethod() === 'PUT');

Response

<?phpuseSignalforge\NativeHttp\Response;
useSignalforge\NativeHttp\Stream;
// Factory methods$response = Response::create(200, ['Content-Type' => 'application/json']);
$response = Response::json(['users' => ['id' => 1, 'name' => 'John']], 200);
$response = Response::text('Hello World', 200);
$response = Response::html('<h1>Welcome</h1>', 200);
$response = Response::redirect('/login', 302);
// Status management$statusCode = $response->getStatusCode(); // 200$reasonPhrase = $response->getReasonPhrase(); // "OK"$response = $response->withStatus(404, 'Not Found');
// Header management (case-insensitive)$response = $response->withHeader('Content-Type', 'application/json');
$response = $response->withAddedHeader('Cache-Control', 'no-cache');
$response = $response->withAddedHeader('Cache-Control', 'private');
$hasHeader = $response->hasHeader('Content-Type'); // true$headerValue = $response->getHeader('Content-Type'); // ['application/json']$headerLine = $response->getHeaderLine('Content-Type'); // "application/json"$allHeaders = $response->getHeaders();
// Body management$stream = Stream::fromString('{"message": "Hello"}');
$response = $response->withBody($stream);
$bodyStream = $response->getBody();
// Output$response->send(); // Send headers + body$response->sendHeaders(); // Send only headers$response->sendBody(); // Send only body// Serialization$message = (string) $response; // Full HTTP message

Stream

<?phpuseSignalforge\NativeHttp\Stream;
// Factory methods$stream = Stream::fromString('Hello World'); // TRUE zero-copy string reference$stream = Stream::fromResource(fopen('file.txt', 'r')); // From PHP resource$stream = Stream::fromFile('/path/to/file', 'r'); // From file path// Reading operations$data = $stream->read(5); // Read 5 bytes: "Hello"$remaining = $stream->getContents(); // Get rest: " World"$stream->rewind(); // Reset to beginning$all = (string) $stream; // Get entire contents// Writing operations (use file or resource streams for writing)$writableStream = Stream::fromFile('/tmp/output.txt', 'w+');
$bytesWritten = $writableStream->write('Hello'); // Write data$writableStream->write(' World'); // Append more// Seeking operations$stream->seek(6); // Seek to position 6$position = $stream->tell(); // Get current position: 6$stream->rewind(); // Reset to beginning// Stream capabilities$isReadable = $stream->isReadable(); // Check if can read$isWritable = $stream->isWritable(); // Check if can write$isSeekable = $stream->isSeekable(); // Check if supports seeking$atEnd = $stream->eof(); // Check if at end// Metadata and size$size = $stream->getSize(); // Size in bytes (or null)$metadata = $stream->getMetadata(); // All metadata$uri = $stream->getMetadata('uri'); // Specific metadata key// Resource management$underlying = $stream->detach(); // Detach PHP resource$stream->close(); // Close stream and free resources

Uri

<?phpuseSignalforge\NativeHttp\Uri;
// Parse a URI string$uri = Uri::fromString('https://user:pass@example.com:8080/path?query=value#fragment');
// Access components (PSR-7 UriInterface)$scheme = $uri->getScheme(); // "https"$userInfo = $uri->getUserInfo(); // "user:pass"$host = $uri->getHost(); // "example.com"$port = $uri->getPort(); // 8080 (null if standard port for scheme)$path = $uri->getPath(); // "/path"$query = $uri->getQuery(); // "query=value"$fragment = $uri->getFragment(); // "fragment"$authority = $uri->getAuthority(); // "user:pass@example.com:8080"// Serialize to string$uriString = (string) $uri; // "https://user:pass@example.com:8080/path?query=value#fragment"// Immutable modifications$newUri = $uri
->withScheme('http')
->withHost('api.example.com')
->withPort(null) // Remove explicit port
->withPath('/v2/users')
->withQuery('limit=10')
->withFragment('');
// Original URI unchangedassert($uri->getHost() === 'example.com');
assert($newUri->getHost() === 'api.example.com');
// Standard ports are normalized to null$httpsUri = Uri::fromString('https://example.com:443/path');
$port = $httpsUri->getPort(); // null (443 is standard for https)

UploadedFile

<?phpuseSignalforge\NativeHttp\Request;
// Get uploaded files from request$request = Request::capture();
$files = $request->getUploadedFiles();
// Single file uploadif (isset($files['avatar'])) {
$file = $files['avatar'];
// File properties$size = $file->getSize(); // Size in bytes$error = $file->getError(); // UPLOAD_ERR_* constant$clientName = $file->getClientFilename(); // Original filename$mimeType = $file->getClientMediaType(); // MIME type// Move file to permanent location$targetPath = '/uploads/avatars/' . uniqid() . '_' . $clientName;
$file->moveTo($targetPath);
// Note: moveTo() can only be called once per UploadedFile
}
// Multiple file uploadif (isset($files['photos'])) {
foreach ($files['photos'] as$photo) {
if ($photo->getError() === UPLOAD_ERR_OK) {
$filename = $photo->getClientFilename();
$photo->moveTo('/uploads/photos/' . $filename);
}
}
}
// Stream access (alternative to moveTo)$stream = $file->getStream();
$content = $stream->getContents();

Advanced Patterns

<?phpuseSignalforge\NativeHttp\{Request, Response, Stream};
// Middleware-style request processingfunctionauthenticate(Request$request): Request
{
$token = $request->getHeaderLine('Authorization');
$userId = validateToken($token);
return$request->withAttribute('user_id', $userId);
}
functionvalidateJson(Request$request): Request
{
$contentType = $request->getHeaderLine('Content-Type');
if (!str_contains($contentType, 'application/json')) {
thrownewInvalidArgumentException('JSON content type required');
}
return$request;
}
// Request processing pipeline$request = Request::capture();
$request = authenticate($request);
$request = validateJson($request);
// JSON API response$data = ['users' => getUsers($request->getAttribute('user_id'))];
$response = Response::json($data, 200);
// CORS headers$response = $response
->withHeader('Access-Control-Allow-Origin', '*')
->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// Conditional responseif ($request->hasHeader('If-None-Match')) {
$etag = $request->getHeaderLine('If-None-Match');
if ($etag === generateEtag($data)) {
$response = $response->withStatus(304); // Not Modified
}
}
$response->send();

API Reference

Request

Factory Methods

Request::capture(): ServerRequestInterface // Capture current request from superglobals

PSR-7 MessageInterface Methods

getProtocolVersion(): string // Get HTTP protocol version (always "1.1" in FastCGI)withProtocolVersion(string $version): static // Return new instance with protocol versiongetHeaders(): array // Get all headers as lowercase key => array valueshasHeader(string $name): bool // Check if header exists (case-insensitive)getHeader(string $name): array // Get header values arraygetHeaderLine(string $name): string // Get header values as comma-separated stringwithHeader(string $name, string|array $value): static // Replace header (case-insensitive)withAddedHeader(string $name, string|array $value): static // Add to existing headerwithoutHeader(string $name): static // Remove headergetBody(): StreamInterface // Get message body streamwithBody(StreamInterface $body): static // Replace body stream

PSR-7 RequestInterface Methods

getRequestTarget(): string // Get request target (path + query)withRequestTarget(string $target): static // Set request targetgetMethod(): string // Get HTTP methodwithMethod(string $method): static // Set HTTP methodgetUri(): string // Get URI as stringwithUri(string|UriInterface $uri, bool $preserveHost = false): static // Set URI

PSR-7 ServerRequestInterface Methods

getServerParams(): array // Get $_SERVER parametersgetCookieParams(): array // Get $_COOKIE parameterswithCookieParams(array $cookies): static // Replace cookiesgetQueryParams(): array // Get $_GET parameterswithQueryParams(array $query): static // Replace query parametersgetUploadedFiles(): array // Get uploaded files structurewithUploadedFiles(array $files): static // Replace uploaded filesgetParsedBody(): array|object|null // Get parsed body (JSON/form data)withParsedBody(array|object|null $data): static // Set parsed bodygetAttributes(): array // Get request attributesgetAttribute(string $name, mixed $default = null) // Get single attributewithAttribute(string $name, mixed $value): static // Add/replace attributewithoutAttribute(string $name): static // Remove attribute

Response

Factory Methods

Response::create(int $status = 200, array $headers = [], mixed $body = null): static
Response::json(mixed $data, int $status = 200): static
Response::text(string $text, int $status = 200): static
Response::html(string $html, int $status = 200): static
Response::redirect(string $url, int $status = 302): static

PSR-7 MessageInterface Methods

getProtocolVersion(): string // Get HTTP protocol versionwithProtocolVersion(string $version): static // Set protocol versiongetHeaders(): array // Get all headershasHeader(string $name): bool // Check header existsgetHeader(string $name): array // Get header valuesgetHeaderLine(string $name): string // Get comma-separated headerwithHeader(string $name, string|array $value): static // Replace headerwithAddedHeader(string $name, string|array $value): static // Add header valuewithoutHeader(string $name): static // Remove headergetBody(): StreamInterface // Get body streamwithBody(StreamInterface $body): static // Replace body stream

PSR-7 ResponseInterface Methods

getStatusCode(): int // Get HTTP status codewithStatus(int $code, string $reason = ''): static // Set status code and reasongetReasonPhrase(): string // Get reason phrase

Output Methods

send(): void // Send response (headers + body)sendHeaders(): void // Send only headerssendBody(): void // Send only body__toString(): string // Serialize to HTTP message

Stream

Factory Methods

Stream::fromString(string $string): static // Create from string (zero-copy)
Stream::fromResource(resource $resource): static // Create from PHP stream resource
Stream::fromFile(string $path, string $mode = 'r'): static // Create from file

PSR-7 StreamInterface Methods

read(int $length): string // Read data from stream
getContents(): string // Get remaining contentswrite(string $string): int // Write data to streamseek(int $offset, int $whence = SEEK_SET): void // Seek to positiontell(): int // Get current positionrewind(): void // Seek to beginningeof(): bool // Check if at end of streamisReadable(): bool // Check if stream is readableisWritable(): bool // Check if stream is writableisSeekable(): bool // Check if stream supports seekinggetSize(): ?int // Get stream size (if known)getMetadata(?string $key = null): mixed // Get stream metadataclose(): void // Close stream and free resourcesdetach(): resource|null // Detach underlying resource__toString(): string // Get entire stream contents

UploadedFile

PSR-7 UploadedFileInterface Methods

getStream(): StreamInterface // Get file contents as streammoveTo(string $targetPath): void // Move file to new locationgetSize(): ?int // Get file size in bytesgetError(): int // Get upload error code (UPLOAD_ERR_*)getClientFilename(): ?string // Get original client filenamegetClientMediaType(): ?string // Get client-provided MIME type

Uri

Factory Methods

Uri::fromString(string $uri): UriInterface // Parse URI string (RFC 3986 compliant)

PSR-7 UriInterface Methods

getScheme(): string // Get URI scheme (http, https, etc.)getAuthority(): string // Get authority (userinfo@host:port)getUserInfo(): string // Get user info (user:pass)getHost(): string // Get host (lowercase)getPort(): ?int // Get port (null if standard for scheme)getPath(): string // Get path componentgetQuery(): string // Get query string (without ?)getFragment(): string // Get fragment (without #)withScheme(string $scheme): UriInterface // Return new instance with schemewithUserInfo(string $user, ?string $pass = null): UriInterface // Set user infowithHost(string $host): UriInterface // Set hostwithPort(?int $port): UriInterface // Set port (null to remove)withPath(string $path): UriInterface // Set pathwithQuery(string $query): UriInterface // Set query stringwithFragment(string $fragment): UriInterface // Set fragment__toString(): string // Serialize to URI string

Performance

The extension provides significant performance improvements over userland PSR-7 implementations through native C code, direct superglobal access, and zero-copy operations. Benchmarks comparing against other PSR-7 implementations can be found in the http-php repository.

Key Optimizations

  • Direct superglobal access - bypasses PHP's array layer for $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
  • Zero-copy string streams - reference strings directly without data duplication
  • Native hash tables - efficient storage and lookup for headers and parameters
  • Lazy evaluation - parse JSON/form data only when accessed
  • Immutable operations - efficient object cloning with shared data structures
  • Memory efficient - proper reference counting and cleanup

How It Works

Request Capture Process

  1. Direct superglobal access - References $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES directly
  2. Lazy header parsing - Headers parsed only when getHeaders() is called
  3. JSON caching - Parsed JSON bodies cached to avoid re-parsing
  4. Immutable cloning - with*() methods create efficient clones with shared data

Stream Operations

  • String streams: TRUE zero-copy references to existing strings (no data duplication)
  • Resource streams: Efficient php_stream_copy_to_mem() for large data
  • Lazy loading: Stream contents read only when accessed
  • Position tracking: Efficient position management for seekable streams

Memory Management

  • Reference counting: Proper Zend reference counting throughout
  • Object pooling: Reuses memory structures where possible
  • Automatic cleanup: Destructors handle resource cleanup
  • Leak prevention: All allocations properly tracked and freed

Structure

http/
├── config.m4 # Build configuration
├── signalforge_http.c # PHP class implementations
├── php_signalforge_http.h # Main header
├── src/
│ ├── request.c/h # Request class implementation
│ ├── response.c/h # Response class implementation
│ ├── stream.c/h # Stream class implementation
│ ├── uri.c/h # Uri class implementation
│ ├── uploadedfile.c/h # UploadedFile class implementation
│ ├── psr7_interfaces.c/h # PSR-7 interface definitions
├── Signalforge/Http/ # IDE stubs
├── examples/ # Usage examples
├── tests/ # phpt test files (97 tests)
└── Dockerfile # Docker build environment

Testing

make test

Or run specific tests:

docker run --rm signalforge-http php /opt/run-tests.php tests/001_basic.phpt

Memory Leak Detection

# Docker-based Valgrind (recommended)
make valgrind-docker
# Local Valgrind (requires valgrind installed)
make valgrind-test

Thread Safety

The extension supports ZTS (Zend Thread Safety) builds. Each request gets isolated instances, and all operations are thread-safe.

Exception Handling

  • InvalidArgumentException - Invalid parameters or malformed data
  • RuntimeException - Stream operations, file access errors
  • Standard PHP exceptions for JSON parsing errors

Related

License

MIT License

About

A native PHP extension implementing a high-performance PSR-7, PSR-17 and PSR-18 compliant HTTP request and response handling - direct HashTable access, zero-copy strings, and lazy evaluation.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Signalforge HTTP Extension

CIPHP 8.3+

A native PHP extension implementing high-performance PSR-7 HTTP Request and Response classes with zero-copy operations and direct superglobal access.

What's Different

  • Native C implementation - all HTTP operations run in native code
  • Zero-copy string streams - reference strings directly without data duplication
  • Direct HashTable access - bypass PHP arrays for superglobal data
  • Lazy evaluation - parse data only when accessed
  • Immutable objects - all with*() methods return new instances
  • Memory efficient - proper reference counting and cleanup
  • PSR-7 compliant - implements ServerRequestInterface, ResponseInterface, StreamInterface, UriInterface, and UploadedFileInterface
  • Optimized for php-fpm - designed for FastCGI environments
  • No dependencies - pure C extension with no external libraries

Why C?

HTTP request/response handling is invoked on nearly every request, often hundreds of times. Moving HTTP operations to native code provides:

  • Direct superglobal access - bypass PHP's array layer for $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
  • Zero-copy string operations - reference string data directly without duplication
  • Native hash tables - efficient storage and lookup for headers and parameters
  • Reduced overhead - minimal PHP engine interaction during data access
  • Memory efficiency - proper reference counting and cleanup
  • Lazy evaluation - parse JSON/form data only when requested
  • Immutable operations - efficient object cloning with shared data structures

Features

  • Full PSR-7 Compliance: Implements ServerRequestInterface, ResponseInterface, StreamInterface, UriInterface, and UploadedFileInterface
  • Zero Dependencies: Pure C extension with no external libraries
  • Hyper-Performance: Direct HashTable access, zero-copy operations, lazy evaluation
  • Immutable Objects: All with*() methods return new instances
  • Memory Efficient: Proper reference counting and cleanup

Streamforge Proxy Integration

The extension integrates seamlessly with the Streamforge FastCGI proxy for high-performance file upload handling. When Streamforge is deployed between nginx and php-fpm, it provides several benefits depending on your nginx configuration.

Understanding nginx Buffering

Important: nginx's fastcgi_request_buffering setting affects what problem streamforge solves:

WITH fastcgi_request_buffering ON (nginx default):
┌────────┐ ┌───────────────┐ ┌─────────┐ ┌───────────┐
│ Client │────▶│ nginx buffers │────▶│ php-fpm │────▶│ $_FILES │
└────────┘ └───────────────┘ └─────────┘ └───────────┘
slow fast │
(to disk) Worker engaged only
during fast transfer
WITH fastcgi_request_buffering OFF:
┌────────┐ ┌───────┐ ┌─────────┐
│ Client │────▶│ nginx │────▶│ php-fpm │ ← Worker blocked for entire upload!
└────────┘ └───────┘ └─────────┘
slow streams
directly

With default nginx settings, workers are already protected from slow clients. The upload is buffered by nginx first.

When Streamforge Helps

nginx settingStreamforge benefit
fastcgi_request_buffering on (default)Avoids double temp file write, reduces PHP memory, skips multipart parsing in PHP
fastcgi_request_buffering offFull benefit: Workers not blocked during slow uploads

With fastcgi_request_buffering off

This is where streamforge shines. Configure nginx to stream directly:

location /upload {fastcgi_request_buffering off; # Stream to backend
fastcgi_pass streamforge:9001;}

Now streamforge handles the slow client I/O:

┌────────┐ ┌─────────────┐ ┌─────────┐
│ Client │────▶│ streamforge │────▶│ php-fpm │
└────────┘ └─────────────┘ └─────────┘
slow writes to disk Worker engaged
as data arrives only ~5ms
ScenarioWithout StreamforgeWith Streamforge
500MB upload, slow clientWorker blocked ~30sWorker engaged ~5ms
20 concurrent uploads, 10 workersSite unresponsiveNo impact

With Default nginx (buffering on)

Streamforge still provides value, just different:

  • No double write: nginx buffer → streamforge disk (not nginx buffer → PHP temp)
  • Less PHP memory: No request body buffering in PHP
  • No multipart parsing: PHP doesn't parse multipart boundaries
  • Consistent API: Same HTTP_X_UPLOAD_* interface regardless of nginx config

Transparent Integration

The extension automatically detects Streamforge and reads uploads from the appropriate source. Your application code remains unchanged:

// Works identically with or without Streamforge$request = Request::capture();
$files = $request->getUploadedFiles();
foreach ($filesas$name => $file) {
$file->getClientFilename(); // "document.pdf"$file->getSize(); // 52428800$file->moveTo('/storage/docs/document.pdf');
}

Detection API

Check if Streamforge is handling the current request:

useSignalforge\NativeHttp\Request;
// Static methodif (Request::isStreamforgeEnabled()) {
// Streamforge is proxying this request
}
// Or check $_SERVER directlyif (isset($_SERVER['HTTP_X_STREAMFORGE'])) {
// Streamforge marker present
}
// Check for processed uploadsif (isset($_SERVER['HTTP_X_UPLOAD_FILE_COUNT'])) {
$count = (int) $_SERVER['HTTP_X_UPLOAD_FILE_COUNT'];
// Streamforge handled $count file uploads
}

Protocol

When Streamforge handles multipart uploads, it:

  1. Parses the multipart body and writes files to disk
  2. Adds metadata headers to the FastCGI request:
    • HTTP_X_STREAMFORGE=1 - Proxy marker
    • HTTP_X_UPLOAD_FILE_COUNT=N - Number of uploaded files
    • HTTP_X_UPLOAD_0_NAME - Form field name
    • HTTP_X_UPLOAD_0_FILENAME - Original client filename
    • HTTP_X_UPLOAD_0_PATH - Temp file path on disk
    • HTTP_X_UPLOAD_0_SIZE - File size in bytes
    • HTTP_X_UPLOAD_0_TYPE - MIME type
  3. Sends only form fields (not file content) to PHP-FPM

The extension reads these headers and creates UploadedFile objects that work identically to standard PHP uploads.

Cleanup

Temp files are automatically cleaned up:

  • On moveTo(): File is moved, no cleanup needed
  • On request end: Unmoved temp files are deleted by the extension's RSHUTDOWN handler

This prevents disk space leaks even if application code doesn't handle all uploaded files.

Deployment

See the Streamforge documentation for deployment instructions. Basic setup:

# Start Streamforge between nginx and php-fpm
streamforge -l 0.0.0.0:9001 -u /var/run/php-fpm.sock -d /tmp/uploads
# Configure nginx to send requests to Streamforge# fastcgi_pass 127.0.0.1:9001;

Requirements

  • PHP 8.3, 8.4, or 8.5
  • Linux or macOS (tested on x86_64 and ARM64)
  • php-fpm recommended (works in CLI for testing)

Building

Docker (Recommended)

No need to install PHP dev headers on your host:

cd http
# Build Docker image with extension
make docker-build
# Run tests
make docker-test
# Test all PHP versions (8.3, 8.4, 8.5)
make ci-test-all
# Run example
make docker-example

Host Installation

cd http
phpize
./configure --enable-signalforge_http
make
make test
sudo make install

Then add extension=signalforge_http.so to your php.ini.

Usage

Request

<?phpuseSignalforge\NativeHttp\Request;
// Capture the current request$request = Request::capture();
// HTTP Method & URI$method = $request->getMethod(); // "POST"$target = $request->getRequestTarget(); // "/users/123?include=profile"$path = $request->getUri(); // "/users/123?include=profile"// Headers (case-insensitive)$contentType = $request->getHeader('Content-Type'); // ['application/json']$contentTypeLine = $request->getHeaderLine('Content-Type'); // "application/json"$hasAuth = $request->hasHeader('Authorization'); // true/false$allHeaders = $request->getHeaders(); // ['content-type' => ['application/json']]// Parameters$queryParams = $request->getQueryParams(); // $_GET as array$parsedBody = $request->getParsedBody(); // JSON/form data (lazy parsed)// Body access$bodyStream = $request->getBody(); // StreamInterface$rawBody = (string) $request->getBody(); // Raw body string// Server & environment$serverParams = $request->getServerParams(); // $_SERVER$userAgent = $serverParams['HTTP_USER_AGENT'];
// Cookies$cookies = $request->getCookieParams(); // $_COOKIE as array$sessionId = $cookies['session_id'];
// Uploaded files$files = $request->getUploadedFiles(); // Normalized file structureif (isset($files['avatar'])) {
$filename = $files['avatar']->getClientFilename();
$files['avatar']->moveTo('/uploads/' . $filename);
}
// Attributes (middleware data)$request = $request->withAttribute('user_id', 123);
$userId = $request->getAttribute('user_id'); // 123$userId = $request->getAttribute('missing', 'default'); // 'default'// Immutable modifications$newRequest = $request
->withMethod('PUT')
->withHeader('X-API-Key', 'secret')
->withQueryParams(['limit' => 10])
->withParsedBody(['name' => 'John']);
// Original request unchangedassert($request->getMethod() === 'POST');
assert($newRequest->getMethod() === 'PUT');

Response

<?phpuseSignalforge\NativeHttp\Response;
useSignalforge\NativeHttp\Stream;
// Factory methods$response = Response::create(200, ['Content-Type' => 'application/json']);
$response = Response::json(['users' => ['id' => 1, 'name' => 'John']], 200);
$response = Response::text('Hello World', 200);
$response = Response::html('<h1>Welcome</h1>', 200);
$response = Response::redirect('/login', 302);
// Status management$statusCode = $response->getStatusCode(); // 200$reasonPhrase = $response->getReasonPhrase(); // "OK"$response = $response->withStatus(404, 'Not Found');
// Header management (case-insensitive)$response = $response->withHeader('Content-Type', 'application/json');
$response = $response->withAddedHeader('Cache-Control', 'no-cache');
$response = $response->withAddedHeader('Cache-Control', 'private');
$hasHeader = $response->hasHeader('Content-Type'); // true$headerValue = $response->getHeader('Content-Type'); // ['application/json']$headerLine = $response->getHeaderLine('Content-Type'); // "application/json"$allHeaders = $response->getHeaders();
// Body management$stream = Stream::fromString('{"message": "Hello"}');
$response = $response->withBody($stream);
$bodyStream = $response->getBody();
// Output$response->send(); // Send headers + body$response->sendHeaders(); // Send only headers$response->sendBody(); // Send only body// Serialization$message = (string) $response; // Full HTTP message

Stream

<?phpuseSignalforge\NativeHttp\Stream;
// Factory methods$stream = Stream::fromString('Hello World'); // TRUE zero-copy string reference$stream = Stream::fromResource(fopen('file.txt', 'r')); // From PHP resource$stream = Stream::fromFile('/path/to/file', 'r'); // From file path// Reading operations$data = $stream->read(5); // Read 5 bytes: "Hello"$remaining = $stream->getContents(); // Get rest: " World"$stream->rewind(); // Reset to beginning$all = (string) $stream; // Get entire contents// Writing operations (use file or resource streams for writing)$writableStream = Stream::fromFile('/tmp/output.txt', 'w+');
$bytesWritten = $writableStream->write('Hello'); // Write data$writableStream->write(' World'); // Append more// Seeking operations$stream->seek(6); // Seek to position 6$position = $stream->tell(); // Get current position: 6$stream->rewind(); // Reset to beginning// Stream capabilities$isReadable = $stream->isReadable(); // Check if can read$isWritable = $stream->isWritable(); // Check if can write$isSeekable = $stream->isSeekable(); // Check if supports seeking$atEnd = $stream->eof(); // Check if at end// Metadata and size$size = $stream->getSize(); // Size in bytes (or null)$metadata = $stream->getMetadata(); // All metadata$uri = $stream->getMetadata('uri'); // Specific metadata key// Resource management$underlying = $stream->detach(); // Detach PHP resource$stream->close(); // Close stream and free resources

Uri

<?phpuseSignalforge\NativeHttp\Uri;
// Parse a URI string$uri = Uri::fromString('https://user:pass@example.com:8080/path?query=value#fragment');
// Access components (PSR-7 UriInterface)$scheme = $uri->getScheme(); // "https"$userInfo = $uri->getUserInfo(); // "user:pass"$host = $uri->getHost(); // "example.com"$port = $uri->getPort(); // 8080 (null if standard port for scheme)$path = $uri->getPath(); // "/path"$query = $uri->getQuery(); // "query=value"$fragment = $uri->getFragment(); // "fragment"$authority = $uri->getAuthority(); // "user:pass@example.com:8080"// Serialize to string$uriString = (string) $uri; // "https://user:pass@example.com:8080/path?query=value#fragment"// Immutable modifications$newUri = $uri
->withScheme('http')
->withHost('api.example.com')
->withPort(null) // Remove explicit port
->withPath('/v2/users')
->withQuery('limit=10')
->withFragment('');
// Original URI unchangedassert($uri->getHost() === 'example.com');
assert($newUri->getHost() === 'api.example.com');
// Standard ports are normalized to null$httpsUri = Uri::fromString('https://example.com:443/path');
$port = $httpsUri->getPort(); // null (443 is standard for https)

UploadedFile

<?phpuseSignalforge\NativeHttp\Request;
// Get uploaded files from request$request = Request::capture();
$files = $request->getUploadedFiles();
// Single file uploadif (isset($files['avatar'])) {
$file = $files['avatar'];
// File properties$size = $file->getSize(); // Size in bytes$error = $file->getError(); // UPLOAD_ERR_* constant$clientName = $file->getClientFilename(); // Original filename$mimeType = $file->getClientMediaType(); // MIME type// Move file to permanent location$targetPath = '/uploads/avatars/' . uniqid() . '_' . $clientName;
$file->moveTo($targetPath);
// Note: moveTo() can only be called once per UploadedFile
}
// Multiple file uploadif (isset($files['photos'])) {
foreach ($files['photos'] as$photo) {
if ($photo->getError() === UPLOAD_ERR_OK) {
$filename = $photo->getClientFilename();
$photo->moveTo('/uploads/photos/' . $filename);
}
}
}
// Stream access (alternative to moveTo)$stream = $file->getStream();
$content = $stream->getContents();

Advanced Patterns

<?phpuseSignalforge\NativeHttp\{Request, Response, Stream};
// Middleware-style request processingfunctionauthenticate(Request$request): Request
{
$token = $request->getHeaderLine('Authorization');
$userId = validateToken($token);
return$request->withAttribute('user_id', $userId);
}
functionvalidateJson(Request$request): Request
{
$contentType = $request->getHeaderLine('Content-Type');
if (!str_contains($contentType, 'application/json')) {
thrownewInvalidArgumentException('JSON content type required');
}
return$request;
}
// Request processing pipeline$request = Request::capture();
$request = authenticate($request);
$request = validateJson($request);
// JSON API response$data = ['users' => getUsers($request->getAttribute('user_id'))];
$response = Response::json($data, 200);
// CORS headers$response = $response
->withHeader('Access-Control-Allow-Origin', '*')
->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// Conditional responseif ($request->hasHeader('If-None-Match')) {
$etag = $request->getHeaderLine('If-None-Match');
if ($etag === generateEtag($data)) {
$response = $response->withStatus(304); // Not Modified
}
}
$response->send();

API Reference

Request

Factory Methods

Request::capture(): ServerRequestInterface // Capture current request from superglobals

PSR-7 MessageInterface Methods

getProtocolVersion(): string // Get HTTP protocol version (always "1.1" in FastCGI)withProtocolVersion(string $version): static // Return new instance with protocol versiongetHeaders(): array // Get all headers as lowercase key => array valueshasHeader(string $name): bool // Check if header exists (case-insensitive)getHeader(string $name): array // Get header values arraygetHeaderLine(string $name): string // Get header values as comma-separated stringwithHeader(string $name, string|array $value): static // Replace header (case-insensitive)withAddedHeader(string $name, string|array $value): static // Add to existing headerwithoutHeader(string $name): static // Remove headergetBody(): StreamInterface // Get message body streamwithBody(StreamInterface $body): static // Replace body stream

PSR-7 RequestInterface Methods

getRequestTarget(): string // Get request target (path + query)withRequestTarget(string $target): static // Set request targetgetMethod(): string // Get HTTP methodwithMethod(string $method): static // Set HTTP methodgetUri(): string // Get URI as stringwithUri(string|UriInterface $uri, bool $preserveHost = false): static // Set URI

PSR-7 ServerRequestInterface Methods

getServerParams(): array // Get $_SERVER parametersgetCookieParams(): array // Get $_COOKIE parameterswithCookieParams(array $cookies): static // Replace cookiesgetQueryParams(): array // Get $_GET parameterswithQueryParams(array $query): static // Replace query parametersgetUploadedFiles(): array // Get uploaded files structurewithUploadedFiles(array $files): static // Replace uploaded filesgetParsedBody(): array|object|null // Get parsed body (JSON/form data)withParsedBody(array|object|null $data): static // Set parsed bodygetAttributes(): array // Get request attributesgetAttribute(string $name, mixed $default = null) // Get single attributewithAttribute(string $name, mixed $value): static // Add/replace attributewithoutAttribute(string $name): static // Remove attribute

Response

Factory Methods

Response::create(int $status = 200, array $headers = [], mixed $body = null): static
Response::json(mixed $data, int $status = 200): static
Response::text(string $text, int $status = 200): static
Response::html(string $html, int $status = 200): static
Response::redirect(string $url, int $status = 302): static

PSR-7 MessageInterface Methods

getProtocolVersion(): string // Get HTTP protocol versionwithProtocolVersion(string $version): static // Set protocol versiongetHeaders(): array // Get all headershasHeader(string $name): bool // Check header existsgetHeader(string $name): array // Get header valuesgetHeaderLine(string $name): string // Get comma-separated headerwithHeader(string $name, string|array $value): static // Replace headerwithAddedHeader(string $name, string|array $value): static // Add header valuewithoutHeader(string $name): static // Remove headergetBody(): StreamInterface // Get body streamwithBody(StreamInterface $body): static // Replace body stream

PSR-7 ResponseInterface Methods

getStatusCode(): int // Get HTTP status codewithStatus(int $code, string $reason = ''): static // Set status code and reasongetReasonPhrase(): string // Get reason phrase

Output Methods

send(): void // Send response (headers + body)sendHeaders(): void // Send only headerssendBody(): void // Send only body__toString(): string // Serialize to HTTP message

Stream

Factory Methods

Stream::fromString(string $string): static // Create from string (zero-copy)
Stream::fromResource(resource $resource): static // Create from PHP stream resource
Stream::fromFile(string $path, string $mode = 'r'): static // Create from file

PSR-7 StreamInterface Methods

read(int $length): string // Read data from stream
getContents(): string // Get remaining contentswrite(string $string): int // Write data to streamseek(int $offset, int $whence = SEEK_SET): void // Seek to positiontell(): int // Get current positionrewind(): void // Seek to beginningeof(): bool // Check if at end of streamisReadable(): bool // Check if stream is readableisWritable(): bool // Check if stream is writableisSeekable(): bool // Check if stream supports seekinggetSize(): ?int // Get stream size (if known)getMetadata(?string $key = null): mixed // Get stream metadataclose(): void // Close stream and free resourcesdetach(): resource|null // Detach underlying resource__toString(): string // Get entire stream contents

UploadedFile

PSR-7 UploadedFileInterface Methods

getStream(): StreamInterface // Get file contents as streammoveTo(string $targetPath): void // Move file to new locationgetSize(): ?int // Get file size in bytesgetError(): int // Get upload error code (UPLOAD_ERR_*)getClientFilename(): ?string // Get original client filenamegetClientMediaType(): ?string // Get client-provided MIME type

Uri

Factory Methods

Uri::fromString(string $uri): UriInterface // Parse URI string (RFC 3986 compliant)

PSR-7 UriInterface Methods

getScheme(): string // Get URI scheme (http, https, etc.)getAuthority(): string // Get authority (userinfo@host:port)getUserInfo(): string // Get user info (user:pass)getHost(): string // Get host (lowercase)getPort(): ?int // Get port (null if standard for scheme)getPath(): string // Get path componentgetQuery(): string // Get query string (without ?)getFragment(): string // Get fragment (without #)withScheme(string $scheme): UriInterface // Return new instance with schemewithUserInfo(string $user, ?string $pass = null): UriInterface // Set user infowithHost(string $host): UriInterface // Set hostwithPort(?int $port): UriInterface // Set port (null to remove)withPath(string $path): UriInterface // Set pathwithQuery(string $query): UriInterface // Set query stringwithFragment(string $fragment): UriInterface // Set fragment__toString(): string // Serialize to URI string

Performance

The extension provides significant performance improvements over userland PSR-7 implementations through native C code, direct superglobal access, and zero-copy operations. Benchmarks comparing against other PSR-7 implementations can be found in the http-php repository.

Key Optimizations

  • Direct superglobal access - bypasses PHP's array layer for $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
  • Zero-copy string streams - reference strings directly without data duplication
  • Native hash tables - efficient storage and lookup for headers and parameters
  • Lazy evaluation - parse JSON/form data only when accessed
  • Immutable operations - efficient object cloning with shared data structures
  • Memory efficient - proper reference counting and cleanup

How It Works

Request Capture Process

  1. Direct superglobal access - References $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES directly
  2. Lazy header parsing - Headers parsed only when getHeaders() is called
  3. JSON caching - Parsed JSON bodies cached to avoid re-parsing
  4. Immutable cloning - with*() methods create efficient clones with shared data

Stream Operations

  • String streams: TRUE zero-copy references to existing strings (no data duplication)
  • Resource streams: Efficient php_stream_copy_to_mem() for large data
  • Lazy loading: Stream contents read only when accessed
  • Position tracking: Efficient position management for seekable streams

Memory Management

  • Reference counting: Proper Zend reference counting throughout
  • Object pooling: Reuses memory structures where possible
  • Automatic cleanup: Destructors handle resource cleanup
  • Leak prevention: All allocations properly tracked and freed

Structure

http/
├── config.m4 # Build configuration
├── signalforge_http.c # PHP class implementations
├── php_signalforge_http.h # Main header
├── src/
│ ├── request.c/h # Request class implementation
│ ├── response.c/h # Response class implementation
│ ├── stream.c/h # Stream class implementation
│ ├── uri.c/h # Uri class implementation
│ ├── uploadedfile.c/h # UploadedFile class implementation
│ ├── psr7_interfaces.c/h # PSR-7 interface definitions
├── Signalforge/Http/ # IDE stubs
├── examples/ # Usage examples
├── tests/ # phpt test files (97 tests)
└── Dockerfile # Docker build environment

Testing

make test

Or run specific tests:

docker run --rm signalforge-http php /opt/run-tests.php tests/001_basic.phpt

Memory Leak Detection

# Docker-based Valgrind (recommended)
make valgrind-docker
# Local Valgrind (requires valgrind installed)
make valgrind-test

Thread Safety

The extension supports ZTS (Zend Thread Safety) builds. Each request gets isolated instances, and all operations are thread-safe.

Exception Handling

  • InvalidArgumentException - Invalid parameters or malformed data
  • RuntimeException - Stream operations, file access errors
  • Standard PHP exceptions for JSON parsing errors

Related

License

MIT License

About

A native PHP extension implementing a high-performance PSR-7, PSR-17 and PSR-18 compliant HTTP request and response handling - direct HashTable access, zero-copy strings, and lazy evaluation.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Signalforge HTTP Extension

CIPHP 8.3+

A native PHP extension implementing high-performance PSR-7 HTTP Request and Response classes with zero-copy operations and direct superglobal access.

What's Different

  • Native C implementation - all HTTP operations run in native code
  • Zero-copy string streams - reference strings directly without data duplication
  • Direct HashTable access - bypass PHP arrays for superglobal data
  • Lazy evaluation - parse data only when accessed
  • Immutable objects - all with*() methods return new instances
  • Memory efficient - proper reference counting and cleanup
  • PSR-7 compliant - implements ServerRequestInterface, ResponseInterface, StreamInterface, UriInterface, and UploadedFileInterface
  • Optimized for php-fpm - designed for FastCGI environments
  • No dependencies - pure C extension with no external libraries

Why C?

HTTP request/response handling is invoked on nearly every request, often hundreds of times. Moving HTTP operations to native code provides:

  • Direct superglobal access - bypass PHP's array layer for $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
  • Zero-copy string operations - reference string data directly without duplication
  • Native hash tables - efficient storage and lookup for headers and parameters
  • Reduced overhead - minimal PHP engine interaction during data access
  • Memory efficiency - proper reference counting and cleanup
  • Lazy evaluation - parse JSON/form data only when requested
  • Immutable operations - efficient object cloning with shared data structures

Features

  • Full PSR-7 Compliance: Implements ServerRequestInterface, ResponseInterface, StreamInterface, UriInterface, and UploadedFileInterface
  • Zero Dependencies: Pure C extension with no external libraries
  • Hyper-Performance: Direct HashTable access, zero-copy operations, lazy evaluation
  • Immutable Objects: All with*() methods return new instances
  • Memory Efficient: Proper reference counting and cleanup

Streamforge Proxy Integration

The extension integrates seamlessly with the Streamforge FastCGI proxy for high-performance file upload handling. When Streamforge is deployed between nginx and php-fpm, it provides several benefits depending on your nginx configuration.

Understanding nginx Buffering

Important: nginx's fastcgi_request_buffering setting affects what problem streamforge solves:

WITH fastcgi_request_buffering ON (nginx default):
┌────────┐ ┌───────────────┐ ┌─────────┐ ┌───────────┐
│ Client │────▶│ nginx buffers │────▶│ php-fpm │────▶│ $_FILES │
└────────┘ └───────────────┘ └─────────┘ └───────────┘
slow fast │
(to disk) Worker engaged only
during fast transfer
WITH fastcgi_request_buffering OFF:
┌────────┐ ┌───────┐ ┌─────────┐
│ Client │────▶│ nginx │────▶│ php-fpm │ ← Worker blocked for entire upload!
└────────┘ └───────┘ └─────────┘
slow streams
directly

With default nginx settings, workers are already protected from slow clients. The upload is buffered by nginx first.

When Streamforge Helps

nginx settingStreamforge benefit
fastcgi_request_buffering on (default)Avoids double temp file write, reduces PHP memory, skips multipart parsing in PHP
fastcgi_request_buffering offFull benefit: Workers not blocked during slow uploads

With fastcgi_request_buffering off

This is where streamforge shines. Configure nginx to stream directly:

location /upload {fastcgi_request_buffering off; # Stream to backend
fastcgi_pass streamforge:9001;}

Now streamforge handles the slow client I/O:

┌────────┐ ┌─────────────┐ ┌─────────┐
│ Client │────▶│ streamforge │────▶│ php-fpm │
└────────┘ └─────────────┘ └─────────┘
slow writes to disk Worker engaged
as data arrives only ~5ms
ScenarioWithout StreamforgeWith Streamforge
500MB upload, slow clientWorker blocked ~30sWorker engaged ~5ms
20 concurrent uploads, 10 workersSite unresponsiveNo impact

With Default nginx (buffering on)

Streamforge still provides value, just different:

  • No double write: nginx buffer → streamforge disk (not nginx buffer → PHP temp)
  • Less PHP memory: No request body buffering in PHP
  • No multipart parsing: PHP doesn't parse multipart boundaries
  • Consistent API: Same HTTP_X_UPLOAD_* interface regardless of nginx config

Transparent Integration

The extension automatically detects Streamforge and reads uploads from the appropriate source. Your application code remains unchanged:

// Works identically with or without Streamforge$request = Request::capture();
$files = $request->getUploadedFiles();
foreach ($filesas$name => $file) {
$file->getClientFilename(); // "document.pdf"$file->getSize(); // 52428800$file->moveTo('/storage/docs/document.pdf');
}

Detection API

Check if Streamforge is handling the current request:

useSignalforge\NativeHttp\Request;
// Static methodif (Request::isStreamforgeEnabled()) {
// Streamforge is proxying this request
}
// Or check $_SERVER directlyif (isset($_SERVER['HTTP_X_STREAMFORGE'])) {
// Streamforge marker present
}
// Check for processed uploadsif (isset($_SERVER['HTTP_X_UPLOAD_FILE_COUNT'])) {
$count = (int) $_SERVER['HTTP_X_UPLOAD_FILE_COUNT'];
// Streamforge handled $count file uploads
}

Protocol

When Streamforge handles multipart uploads, it:

  1. Parses the multipart body and writes files to disk
  2. Adds metadata headers to the FastCGI request:
    • HTTP_X_STREAMFORGE=1 - Proxy marker
    • HTTP_X_UPLOAD_FILE_COUNT=N - Number of uploaded files
    • HTTP_X_UPLOAD_0_NAME - Form field name
    • HTTP_X_UPLOAD_0_FILENAME - Original client filename
    • HTTP_X_UPLOAD_0_PATH - Temp file path on disk
    • HTTP_X_UPLOAD_0_SIZE - File size in bytes
    • HTTP_X_UPLOAD_0_TYPE - MIME type
  3. Sends only form fields (not file content) to PHP-FPM

The extension reads these headers and creates UploadedFile objects that work identically to standard PHP uploads.

Cleanup

Temp files are automatically cleaned up:

  • On moveTo(): File is moved, no cleanup needed
  • On request end: Unmoved temp files are deleted by the extension's RSHUTDOWN handler

This prevents disk space leaks even if application code doesn't handle all uploaded files.

Deployment

See the Streamforge documentation for deployment instructions. Basic setup:

# Start Streamforge between nginx and php-fpm
streamforge -l 0.0.0.0:9001 -u /var/run/php-fpm.sock -d /tmp/uploads
# Configure nginx to send requests to Streamforge# fastcgi_pass 127.0.0.1:9001;

Requirements

  • PHP 8.3, 8.4, or 8.5
  • Linux or macOS (tested on x86_64 and ARM64)
  • php-fpm recommended (works in CLI for testing)

Building

Docker (Recommended)

No need to install PHP dev headers on your host:

cd http
# Build Docker image with extension
make docker-build
# Run tests
make docker-test
# Test all PHP versions (8.3, 8.4, 8.5)
make ci-test-all
# Run example
make docker-example

Host Installation

cd http
phpize
./configure --enable-signalforge_http
make
make test
sudo make install

Then add extension=signalforge_http.so to your php.ini.

Usage

Request

<?phpuseSignalforge\NativeHttp\Request;
// Capture the current request$request = Request::capture();
// HTTP Method & URI$method = $request->getMethod(); // "POST"$target = $request->getRequestTarget(); // "/users/123?include=profile"$path = $request->getUri(); // "/users/123?include=profile"// Headers (case-insensitive)$contentType = $request->getHeader('Content-Type'); // ['application/json']$contentTypeLine = $request->getHeaderLine('Content-Type'); // "application/json"$hasAuth = $request->hasHeader('Authorization'); // true/false$allHeaders = $request->getHeaders(); // ['content-type' => ['application/json']]// Parameters$queryParams = $request->getQueryParams(); // $_GET as array$parsedBody = $request->getParsedBody(); // JSON/form data (lazy parsed)// Body access$bodyStream = $request->getBody(); // StreamInterface$rawBody = (string) $request->getBody(); // Raw body string// Server & environment$serverParams = $request->getServerParams(); // $_SERVER$userAgent = $serverParams['HTTP_USER_AGENT'];
// Cookies$cookies = $request->getCookieParams(); // $_COOKIE as array$sessionId = $cookies['session_id'];
// Uploaded files$files = $request->getUploadedFiles(); // Normalized file structureif (isset($files['avatar'])) {
$filename = $files['avatar']->getClientFilename();
$files['avatar']->moveTo('/uploads/' . $filename);
}
// Attributes (middleware data)$request = $request->withAttribute('user_id', 123);
$userId = $request->getAttribute('user_id'); // 123$userId = $request->getAttribute('missing', 'default'); // 'default'// Immutable modifications$newRequest = $request
->withMethod('PUT')
->withHeader('X-API-Key', 'secret')
->withQueryParams(['limit' => 10])
->withParsedBody(['name' => 'John']);
// Original request unchangedassert($request->getMethod() === 'POST');
assert($newRequest->getMethod() === 'PUT');

Response

<?phpuseSignalforge\NativeHttp\Response;
useSignalforge\NativeHttp\Stream;
// Factory methods$response = Response::create(200, ['Content-Type' => 'application/json']);
$response = Response::json(['users' => ['id' => 1, 'name' => 'John']], 200);
$response = Response::text('Hello World', 200);
$response = Response::html('<h1>Welcome</h1>', 200);
$response = Response::redirect('/login', 302);
// Status management$statusCode = $response->getStatusCode(); // 200$reasonPhrase = $response->getReasonPhrase(); // "OK"$response = $response->withStatus(404, 'Not Found');
// Header management (case-insensitive)$response = $response->withHeader('Content-Type', 'application/json');
$response = $response->withAddedHeader('Cache-Control', 'no-cache');
$response = $response->withAddedHeader('Cache-Control', 'private');
$hasHeader = $response->hasHeader('Content-Type'); // true$headerValue = $response->getHeader('Content-Type'); // ['application/json']$headerLine = $response->getHeaderLine('Content-Type'); // "application/json"$allHeaders = $response->getHeaders();
// Body management$stream = Stream::fromString('{"message": "Hello"}');
$response = $response->withBody($stream);
$bodyStream = $response->getBody();
// Output$response->send(); // Send headers + body$response->sendHeaders(); // Send only headers$response->sendBody(); // Send only body// Serialization$message = (string) $response; // Full HTTP message

Stream

<?phpuseSignalforge\NativeHttp\Stream;
// Factory methods$stream = Stream::fromString('Hello World'); // TRUE zero-copy string reference$stream = Stream::fromResource(fopen('file.txt', 'r')); // From PHP resource$stream = Stream::fromFile('/path/to/file', 'r'); // From file path// Reading operations$data = $stream->read(5); // Read 5 bytes: "Hello"$remaining = $stream->getContents(); // Get rest: " World"$stream->rewind(); // Reset to beginning$all = (string) $stream; // Get entire contents// Writing operations (use file or resource streams for writing)$writableStream = Stream::fromFile('/tmp/output.txt', 'w+');
$bytesWritten = $writableStream->write('Hello'); // Write data$writableStream->write(' World'); // Append more// Seeking operations$stream->seek(6); // Seek to position 6$position = $stream->tell(); // Get current position: 6$stream->rewind(); // Reset to beginning// Stream capabilities$isReadable = $stream->isReadable(); // Check if can read$isWritable = $stream->isWritable(); // Check if can write$isSeekable = $stream->isSeekable(); // Check if supports seeking$atEnd = $stream->eof(); // Check if at end// Metadata and size$size = $stream->getSize(); // Size in bytes (or null)$metadata = $stream->getMetadata(); // All metadata$uri = $stream->getMetadata('uri'); // Specific metadata key// Resource management$underlying = $stream->detach(); // Detach PHP resource$stream->close(); // Close stream and free resources

Uri

<?phpuseSignalforge\NativeHttp\Uri;
// Parse a URI string$uri = Uri::fromString('https://user:pass@example.com:8080/path?query=value#fragment');
// Access components (PSR-7 UriInterface)$scheme = $uri->getScheme(); // "https"$userInfo = $uri->getUserInfo(); // "user:pass"$host = $uri->getHost(); // "example.com"$port = $uri->getPort(); // 8080 (null if standard port for scheme)$path = $uri->getPath(); // "/path"$query = $uri->getQuery(); // "query=value"$fragment = $uri->getFragment(); // "fragment"$authority = $uri->getAuthority(); // "user:pass@example.com:8080"// Serialize to string$uriString = (string) $uri; // "https://user:pass@example.com:8080/path?query=value#fragment"// Immutable modifications$newUri = $uri
->withScheme('http')
->withHost('api.example.com')
->withPort(null) // Remove explicit port
->withPath('/v2/users')
->withQuery('limit=10')
->withFragment('');
// Original URI unchangedassert($uri->getHost() === 'example.com');
assert($newUri->getHost() === 'api.example.com');
// Standard ports are normalized to null$httpsUri = Uri::fromString('https://example.com:443/path');
$port = $httpsUri->getPort(); // null (443 is standard for https)

UploadedFile

<?phpuseSignalforge\NativeHttp\Request;
// Get uploaded files from request$request = Request::capture();
$files = $request->getUploadedFiles();
// Single file uploadif (isset($files['avatar'])) {
$file = $files['avatar'];
// File properties$size = $file->getSize(); // Size in bytes$error = $file->getError(); // UPLOAD_ERR_* constant$clientName = $file->getClientFilename(); // Original filename$mimeType = $file->getClientMediaType(); // MIME type// Move file to permanent location$targetPath = '/uploads/avatars/' . uniqid() . '_' . $clientName;
$file->moveTo($targetPath);
// Note: moveTo() can only be called once per UploadedFile
}
// Multiple file uploadif (isset($files['photos'])) {
foreach ($files['photos'] as$photo) {
if ($photo->getError() === UPLOAD_ERR_OK) {
$filename = $photo->getClientFilename();
$photo->moveTo('/uploads/photos/' . $filename);
}
}
}
// Stream access (alternative to moveTo)$stream = $file->getStream();
$content = $stream->getContents();

Advanced Patterns

<?phpuseSignalforge\NativeHttp\{Request, Response, Stream};
// Middleware-style request processingfunctionauthenticate(Request$request): Request
{
$token = $request->getHeaderLine('Authorization');
$userId = validateToken($token);
return$request->withAttribute('user_id', $userId);
}
functionvalidateJson(Request$request): Request
{
$contentType = $request->getHeaderLine('Content-Type');
if (!str_contains($contentType, 'application/json')) {
thrownewInvalidArgumentException('JSON content type required');
}
return$request;
}
// Request processing pipeline$request = Request::capture();
$request = authenticate($request);
$request = validateJson($request);
// JSON API response$data = ['users' => getUsers($request->getAttribute('user_id'))];
$response = Response::json($data, 200);
// CORS headers$response = $response
->withHeader('Access-Control-Allow-Origin', '*')
->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// Conditional responseif ($request->hasHeader('If-None-Match')) {
$etag = $request->getHeaderLine('If-None-Match');
if ($etag === generateEtag($data)) {
$response = $response->withStatus(304); // Not Modified
}
}
$response->send();

API Reference

Request

Factory Methods

Request::capture(): ServerRequestInterface // Capture current request from superglobals

PSR-7 MessageInterface Methods

getProtocolVersion(): string // Get HTTP protocol version (always "1.1" in FastCGI)withProtocolVersion(string $version): static // Return new instance with protocol versiongetHeaders(): array // Get all headers as lowercase key => array valueshasHeader(string $name): bool // Check if header exists (case-insensitive)getHeader(string $name): array // Get header values arraygetHeaderLine(string $name): string // Get header values as comma-separated stringwithHeader(string $name, string|array $value): static // Replace header (case-insensitive)withAddedHeader(string $name, string|array $value): static // Add to existing headerwithoutHeader(string $name): static // Remove headergetBody(): StreamInterface // Get message body streamwithBody(StreamInterface $body): static // Replace body stream

PSR-7 RequestInterface Methods

getRequestTarget(): string // Get request target (path + query)withRequestTarget(string $target): static // Set request targetgetMethod(): string // Get HTTP methodwithMethod(string $method): static // Set HTTP methodgetUri(): string // Get URI as stringwithUri(string|UriInterface $uri, bool $preserveHost = false): static // Set URI

PSR-7 ServerRequestInterface Methods

getServerParams(): array // Get $_SERVER parametersgetCookieParams(): array // Get $_COOKIE parameterswithCookieParams(array $cookies): static // Replace cookiesgetQueryParams(): array // Get $_GET parameterswithQueryParams(array $query): static // Replace query parametersgetUploadedFiles(): array // Get uploaded files structurewithUploadedFiles(array $files): static // Replace uploaded filesgetParsedBody(): array|object|null // Get parsed body (JSON/form data)withParsedBody(array|object|null $data): static // Set parsed bodygetAttributes(): array // Get request attributesgetAttribute(string $name, mixed $default = null) // Get single attributewithAttribute(string $name, mixed $value): static // Add/replace attributewithoutAttribute(string $name): static // Remove attribute

Response

Factory Methods

Response::create(int $status = 200, array $headers = [], mixed $body = null): static
Response::json(mixed $data, int $status = 200): static
Response::text(string $text, int $status = 200): static
Response::html(string $html, int $status = 200): static
Response::redirect(string $url, int $status = 302): static

PSR-7 MessageInterface Methods

getProtocolVersion(): string // Get HTTP protocol versionwithProtocolVersion(string $version): static // Set protocol versiongetHeaders(): array // Get all headershasHeader(string $name): bool // Check header existsgetHeader(string $name): array // Get header valuesgetHeaderLine(string $name): string // Get comma-separated headerwithHeader(string $name, string|array $value): static // Replace headerwithAddedHeader(string $name, string|array $value): static // Add header valuewithoutHeader(string $name): static // Remove headergetBody(): StreamInterface // Get body streamwithBody(StreamInterface $body): static // Replace body stream

PSR-7 ResponseInterface Methods

getStatusCode(): int // Get HTTP status codewithStatus(int $code, string $reason = ''): static // Set status code and reasongetReasonPhrase(): string // Get reason phrase

Output Methods

send(): void // Send response (headers + body)sendHeaders(): void // Send only headerssendBody(): void // Send only body__toString(): string // Serialize to HTTP message

Stream

Factory Methods

Stream::fromString(string $string): static // Create from string (zero-copy)
Stream::fromResource(resource $resource): static // Create from PHP stream resource
Stream::fromFile(string $path, string $mode = 'r'): static // Create from file

PSR-7 StreamInterface Methods

read(int $length): string // Read data from stream
getContents(): string // Get remaining contentswrite(string $string): int // Write data to streamseek(int $offset, int $whence = SEEK_SET): void // Seek to positiontell(): int // Get current positionrewind(): void // Seek to beginningeof(): bool // Check if at end of streamisReadable(): bool // Check if stream is readableisWritable(): bool // Check if stream is writableisSeekable(): bool // Check if stream supports seekinggetSize(): ?int // Get stream size (if known)getMetadata(?string $key = null): mixed // Get stream metadataclose(): void // Close stream and free resourcesdetach(): resource|null // Detach underlying resource__toString(): string // Get entire stream contents

UploadedFile

PSR-7 UploadedFileInterface Methods

getStream(): StreamInterface // Get file contents as streammoveTo(string $targetPath): void // Move file to new locationgetSize(): ?int // Get file size in bytesgetError(): int // Get upload error code (UPLOAD_ERR_*)getClientFilename(): ?string // Get original client filenamegetClientMediaType(): ?string // Get client-provided MIME type

Uri

Factory Methods

Uri::fromString(string $uri): UriInterface // Parse URI string (RFC 3986 compliant)

PSR-7 UriInterface Methods

getScheme(): string // Get URI scheme (http, https, etc.)getAuthority(): string // Get authority (userinfo@host:port)getUserInfo(): string // Get user info (user:pass)getHost(): string // Get host (lowercase)getPort(): ?int // Get port (null if standard for scheme)getPath(): string // Get path componentgetQuery(): string // Get query string (without ?)getFragment(): string // Get fragment (without #)withScheme(string $scheme): UriInterface // Return new instance with schemewithUserInfo(string $user, ?string $pass = null): UriInterface // Set user infowithHost(string $host): UriInterface // Set hostwithPort(?int $port): UriInterface // Set port (null to remove)withPath(string $path): UriInterface // Set pathwithQuery(string $query): UriInterface // Set query stringwithFragment(string $fragment): UriInterface // Set fragment__toString(): string // Serialize to URI string

Performance

The extension provides significant performance improvements over userland PSR-7 implementations through native C code, direct superglobal access, and zero-copy operations. Benchmarks comparing against other PSR-7 implementations can be found in the http-php repository.

Key Optimizations

  • Direct superglobal access - bypasses PHP's array layer for $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
  • Zero-copy string streams - reference strings directly without data duplication
  • Native hash tables - efficient storage and lookup for headers and parameters
  • Lazy evaluation - parse JSON/form data only when accessed
  • Immutable operations - efficient object cloning with shared data structures
  • Memory efficient - proper reference counting and cleanup

How It Works

Request Capture Process

  1. Direct superglobal access - References $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES directly
  2. Lazy header parsing - Headers parsed only when getHeaders() is called
  3. JSON caching - Parsed JSON bodies cached to avoid re-parsing
  4. Immutable cloning - with*() methods create efficient clones with shared data

Stream Operations

  • String streams: TRUE zero-copy references to existing strings (no data duplication)
  • Resource streams: Efficient php_stream_copy_to_mem() for large data
  • Lazy loading: Stream contents read only when accessed
  • Position tracking: Efficient position management for seekable streams

Memory Management

  • Reference counting: Proper Zend reference counting throughout
  • Object pooling: Reuses memory structures where possible
  • Automatic cleanup: Destructors handle resource cleanup
  • Leak prevention: All allocations properly tracked and freed

Structure

http/
├── config.m4 # Build configuration
├── signalforge_http.c # PHP class implementations
├── php_signalforge_http.h # Main header
├── src/
│ ├── request.c/h # Request class implementation
│ ├── response.c/h # Response class implementation
│ ├── stream.c/h # Stream class implementation
│ ├── uri.c/h # Uri class implementation
│ ├── uploadedfile.c/h # UploadedFile class implementation
│ ├── psr7_interfaces.c/h # PSR-7 interface definitions
├── Signalforge/Http/ # IDE stubs
├── examples/ # Usage examples
├── tests/ # phpt test files (97 tests)
└── Dockerfile # Docker build environment

Testing

make test

Or run specific tests:

docker run --rm signalforge-http php /opt/run-tests.php tests/001_basic.phpt

Memory Leak Detection

# Docker-based Valgrind (recommended)
make valgrind-docker
# Local Valgrind (requires valgrind installed)
make valgrind-test

Thread Safety

The extension supports ZTS (Zend Thread Safety) builds. Each request gets isolated instances, and all operations are thread-safe.

Exception Handling

  • InvalidArgumentException - Invalid parameters or malformed data
  • RuntimeException - Stream operations, file access errors
  • Standard PHP exceptions for JSON parsing errors

Related

License

MIT License

About

A native PHP extension implementing a high-performance PSR-7, PSR-17 and PSR-18 compliant HTTP request and response handling - direct HashTable access, zero-copy strings, and lazy evaluation.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Signalforge HTTP Extension

CIPHP 8.3+

A native PHP extension implementing high-performance PSR-7 HTTP Request and Response classes with zero-copy operations and direct superglobal access.

What's Different

  • Native C implementation - all HTTP operations run in native code
  • Zero-copy string streams - reference strings directly without data duplication
  • Direct HashTable access - bypass PHP arrays for superglobal data
  • Lazy evaluation - parse data only when accessed
  • Immutable objects - all with*() methods return new instances
  • Memory efficient - proper reference counting and cleanup
  • PSR-7 compliant - implements ServerRequestInterface, ResponseInterface, StreamInterface, UriInterface, and UploadedFileInterface
  • Optimized for php-fpm - designed for FastCGI environments
  • No dependencies - pure C extension with no external libraries

Why C?

HTTP request/response handling is invoked on nearly every request, often hundreds of times. Moving HTTP operations to native code provides:

  • Direct superglobal access - bypass PHP's array layer for $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
  • Zero-copy string operations - reference string data directly without duplication
  • Native hash tables - efficient storage and lookup for headers and parameters
  • Reduced overhead - minimal PHP engine interaction during data access
  • Memory efficiency - proper reference counting and cleanup
  • Lazy evaluation - parse JSON/form data only when requested
  • Immutable operations - efficient object cloning with shared data structures

Features

  • Full PSR-7 Compliance: Implements ServerRequestInterface, ResponseInterface, StreamInterface, UriInterface, and UploadedFileInterface
  • Zero Dependencies: Pure C extension with no external libraries
  • Hyper-Performance: Direct HashTable access, zero-copy operations, lazy evaluation
  • Immutable Objects: All with*() methods return new instances
  • Memory Efficient: Proper reference counting and cleanup

Streamforge Proxy Integration

The extension integrates seamlessly with the Streamforge FastCGI proxy for high-performance file upload handling. When Streamforge is deployed between nginx and php-fpm, it provides several benefits depending on your nginx configuration.

Understanding nginx Buffering

Important: nginx's fastcgi_request_buffering setting affects what problem streamforge solves:

WITH fastcgi_request_buffering ON (nginx default):
┌────────┐ ┌───────────────┐ ┌─────────┐ ┌───────────┐
│ Client │────▶│ nginx buffers │────▶│ php-fpm │────▶│ $_FILES │
└────────┘ └───────────────┘ └─────────┘ └───────────┘
slow fast │
(to disk) Worker engaged only
during fast transfer
WITH fastcgi_request_buffering OFF:
┌────────┐ ┌───────┐ ┌─────────┐
│ Client │────▶│ nginx │────▶│ php-fpm │ ← Worker blocked for entire upload!
└────────┘ └───────┘ └─────────┘
slow streams
directly

With default nginx settings, workers are already protected from slow clients. The upload is buffered by nginx first.

When Streamforge Helps

nginx settingStreamforge benefit
fastcgi_request_buffering on (default)Avoids double temp file write, reduces PHP memory, skips multipart parsing in PHP
fastcgi_request_buffering offFull benefit: Workers not blocked during slow uploads

With fastcgi_request_buffering off

This is where streamforge shines. Configure nginx to stream directly:

location /upload {fastcgi_request_buffering off; # Stream to backend
fastcgi_pass streamforge:9001;}

Now streamforge handles the slow client I/O:

┌────────┐ ┌─────────────┐ ┌─────────┐
│ Client │────▶│ streamforge │────▶│ php-fpm │
└────────┘ └─────────────┘ └─────────┘
slow writes to disk Worker engaged
as data arrives only ~5ms
ScenarioWithout StreamforgeWith Streamforge
500MB upload, slow clientWorker blocked ~30sWorker engaged ~5ms
20 concurrent uploads, 10 workersSite unresponsiveNo impact

With Default nginx (buffering on)

Streamforge still provides value, just different:

  • No double write: nginx buffer → streamforge disk (not nginx buffer → PHP temp)
  • Less PHP memory: No request body buffering in PHP
  • No multipart parsing: PHP doesn't parse multipart boundaries
  • Consistent API: Same HTTP_X_UPLOAD_* interface regardless of nginx config

Transparent Integration

The extension automatically detects Streamforge and reads uploads from the appropriate source. Your application code remains unchanged:

// Works identically with or without Streamforge$request = Request::capture();
$files = $request->getUploadedFiles();
foreach ($filesas$name => $file) {
$file->getClientFilename(); // "document.pdf"$file->getSize(); // 52428800$file->moveTo('/storage/docs/document.pdf');
}

Detection API

Check if Streamforge is handling the current request:

useSignalforge\NativeHttp\Request;
// Static methodif (Request::isStreamforgeEnabled()) {
// Streamforge is proxying this request
}
// Or check $_SERVER directlyif (isset($_SERVER['HTTP_X_STREAMFORGE'])) {
// Streamforge marker present
}
// Check for processed uploadsif (isset($_SERVER['HTTP_X_UPLOAD_FILE_COUNT'])) {
$count = (int) $_SERVER['HTTP_X_UPLOAD_FILE_COUNT'];
// Streamforge handled $count file uploads
}

Protocol

When Streamforge handles multipart uploads, it:

  1. Parses the multipart body and writes files to disk
  2. Adds metadata headers to the FastCGI request:
    • HTTP_X_STREAMFORGE=1 - Proxy marker
    • HTTP_X_UPLOAD_FILE_COUNT=N - Number of uploaded files
    • HTTP_X_UPLOAD_0_NAME - Form field name
    • HTTP_X_UPLOAD_0_FILENAME - Original client filename
    • HTTP_X_UPLOAD_0_PATH - Temp file path on disk
    • HTTP_X_UPLOAD_0_SIZE - File size in bytes
    • HTTP_X_UPLOAD_0_TYPE - MIME type
  3. Sends only form fields (not file content) to PHP-FPM

The extension reads these headers and creates UploadedFile objects that work identically to standard PHP uploads.

Cleanup

Temp files are automatically cleaned up:

  • On moveTo(): File is moved, no cleanup needed
  • On request end: Unmoved temp files are deleted by the extension's RSHUTDOWN handler

This prevents disk space leaks even if application code doesn't handle all uploaded files.

Deployment

See the Streamforge documentation for deployment instructions. Basic setup:

# Start Streamforge between nginx and php-fpm
streamforge -l 0.0.0.0:9001 -u /var/run/php-fpm.sock -d /tmp/uploads
# Configure nginx to send requests to Streamforge# fastcgi_pass 127.0.0.1:9001;

Requirements

  • PHP 8.3, 8.4, or 8.5
  • Linux or macOS (tested on x86_64 and ARM64)
  • php-fpm recommended (works in CLI for testing)

Building

Docker (Recommended)

No need to install PHP dev headers on your host:

cd http
# Build Docker image with extension
make docker-build
# Run tests
make docker-test
# Test all PHP versions (8.3, 8.4, 8.5)
make ci-test-all
# Run example
make docker-example

Host Installation

cd http
phpize
./configure --enable-signalforge_http
make
make test
sudo make install

Then add extension=signalforge_http.so to your php.ini.

Usage

Request

<?phpuseSignalforge\NativeHttp\Request;
// Capture the current request$request = Request::capture();
// HTTP Method & URI$method = $request->getMethod(); // "POST"$target = $request->getRequestTarget(); // "/users/123?include=profile"$path = $request->getUri(); // "/users/123?include=profile"// Headers (case-insensitive)$contentType = $request->getHeader('Content-Type'); // ['application/json']$contentTypeLine = $request->getHeaderLine('Content-Type'); // "application/json"$hasAuth = $request->hasHeader('Authorization'); // true/false$allHeaders = $request->getHeaders(); // ['content-type' => ['application/json']]// Parameters$queryParams = $request->getQueryParams(); // $_GET as array$parsedBody = $request->getParsedBody(); // JSON/form data (lazy parsed)// Body access$bodyStream = $request->getBody(); // StreamInterface$rawBody = (string) $request->getBody(); // Raw body string// Server & environment$serverParams = $request->getServerParams(); // $_SERVER$userAgent = $serverParams['HTTP_USER_AGENT'];
// Cookies$cookies = $request->getCookieParams(); // $_COOKIE as array$sessionId = $cookies['session_id'];
// Uploaded files$files = $request->getUploadedFiles(); // Normalized file structureif (isset($files['avatar'])) {
$filename = $files['avatar']->getClientFilename();
$files['avatar']->moveTo('/uploads/' . $filename);
}
// Attributes (middleware data)$request = $request->withAttribute('user_id', 123);
$userId = $request->getAttribute('user_id'); // 123$userId = $request->getAttribute('missing', 'default'); // 'default'// Immutable modifications$newRequest = $request
->withMethod('PUT')
->withHeader('X-API-Key', 'secret')
->withQueryParams(['limit' => 10])
->withParsedBody(['name' => 'John']);
// Original request unchangedassert($request->getMethod() === 'POST');
assert($newRequest->getMethod() === 'PUT');

Response

<?phpuseSignalforge\NativeHttp\Response;
useSignalforge\NativeHttp\Stream;
// Factory methods$response = Response::create(200, ['Content-Type' => 'application/json']);
$response = Response::json(['users' => ['id' => 1, 'name' => 'John']], 200);
$response = Response::text('Hello World', 200);
$response = Response::html('<h1>Welcome</h1>', 200);
$response = Response::redirect('/login', 302);
// Status management$statusCode = $response->getStatusCode(); // 200$reasonPhrase = $response->getReasonPhrase(); // "OK"$response = $response->withStatus(404, 'Not Found');
// Header management (case-insensitive)$response = $response->withHeader('Content-Type', 'application/json');
$response = $response->withAddedHeader('Cache-Control', 'no-cache');
$response = $response->withAddedHeader('Cache-Control', 'private');
$hasHeader = $response->hasHeader('Content-Type'); // true$headerValue = $response->getHeader('Content-Type'); // ['application/json']$headerLine = $response->getHeaderLine('Content-Type'); // "application/json"$allHeaders = $response->getHeaders();
// Body management$stream = Stream::fromString('{"message": "Hello"}');
$response = $response->withBody($stream);
$bodyStream = $response->getBody();
// Output$response->send(); // Send headers + body$response->sendHeaders(); // Send only headers$response->sendBody(); // Send only body// Serialization$message = (string) $response; // Full HTTP message

Stream

<?phpuseSignalforge\NativeHttp\Stream;
// Factory methods$stream = Stream::fromString('Hello World'); // TRUE zero-copy string reference$stream = Stream::fromResource(fopen('file.txt', 'r')); // From PHP resource$stream = Stream::fromFile('/path/to/file', 'r'); // From file path// Reading operations$data = $stream->read(5); // Read 5 bytes: "Hello"$remaining = $stream->getContents(); // Get rest: " World"$stream->rewind(); // Reset to beginning$all = (string) $stream; // Get entire contents// Writing operations (use file or resource streams for writing)$writableStream = Stream::fromFile('/tmp/output.txt', 'w+');
$bytesWritten = $writableStream->write('Hello'); // Write data$writableStream->write(' World'); // Append more// Seeking operations$stream->seek(6); // Seek to position 6$position = $stream->tell(); // Get current position: 6$stream->rewind(); // Reset to beginning// Stream capabilities$isReadable = $stream->isReadable(); // Check if can read$isWritable = $stream->isWritable(); // Check if can write$isSeekable = $stream->isSeekable(); // Check if supports seeking$atEnd = $stream->eof(); // Check if at end// Metadata and size$size = $stream->getSize(); // Size in bytes (or null)$metadata = $stream->getMetadata(); // All metadata$uri = $stream->getMetadata('uri'); // Specific metadata key// Resource management$underlying = $stream->detach(); // Detach PHP resource$stream->close(); // Close stream and free resources

Uri

<?phpuseSignalforge\NativeHttp\Uri;
// Parse a URI string$uri = Uri::fromString('https://user:pass@example.com:8080/path?query=value#fragment');
// Access components (PSR-7 UriInterface)$scheme = $uri->getScheme(); // "https"$userInfo = $uri->getUserInfo(); // "user:pass"$host = $uri->getHost(); // "example.com"$port = $uri->getPort(); // 8080 (null if standard port for scheme)$path = $uri->getPath(); // "/path"$query = $uri->getQuery(); // "query=value"$fragment = $uri->getFragment(); // "fragment"$authority = $uri->getAuthority(); // "user:pass@example.com:8080"// Serialize to string$uriString = (string) $uri; // "https://user:pass@example.com:8080/path?query=value#fragment"// Immutable modifications$newUri = $uri
->withScheme('http')
->withHost('api.example.com')
->withPort(null) // Remove explicit port
->withPath('/v2/users')
->withQuery('limit=10')
->withFragment('');
// Original URI unchangedassert($uri->getHost() === 'example.com');
assert($newUri->getHost() === 'api.example.com');
// Standard ports are normalized to null$httpsUri = Uri::fromString('https://example.com:443/path');
$port = $httpsUri->getPort(); // null (443 is standard for https)

UploadedFile

<?phpuseSignalforge\NativeHttp\Request;
// Get uploaded files from request$request = Request::capture();
$files = $request->getUploadedFiles();
// Single file uploadif (isset($files['avatar'])) {
$file = $files['avatar'];
// File properties$size = $file->getSize(); // Size in bytes$error = $file->getError(); // UPLOAD_ERR_* constant$clientName = $file->getClientFilename(); // Original filename$mimeType = $file->getClientMediaType(); // MIME type// Move file to permanent location$targetPath = '/uploads/avatars/' . uniqid() . '_' . $clientName;
$file->moveTo($targetPath);
// Note: moveTo() can only be called once per UploadedFile
}
// Multiple file uploadif (isset($files['photos'])) {
foreach ($files['photos'] as$photo) {
if ($photo->getError() === UPLOAD_ERR_OK) {
$filename = $photo->getClientFilename();
$photo->moveTo('/uploads/photos/' . $filename);
}
}
}
// Stream access (alternative to moveTo)$stream = $file->getStream();
$content = $stream->getContents();

Advanced Patterns

<?phpuseSignalforge\NativeHttp\{Request, Response, Stream};
// Middleware-style request processingfunctionauthenticate(Request$request): Request
{
$token = $request->getHeaderLine('Authorization');
$userId = validateToken($token);
return$request->withAttribute('user_id', $userId);
}
functionvalidateJson(Request$request): Request
{
$contentType = $request->getHeaderLine('Content-Type');
if (!str_contains($contentType, 'application/json')) {
thrownewInvalidArgumentException('JSON content type required');
}
return$request;
}
// Request processing pipeline$request = Request::capture();
$request = authenticate($request);
$request = validateJson($request);
// JSON API response$data = ['users' => getUsers($request->getAttribute('user_id'))];
$response = Response::json($data, 200);
// CORS headers$response = $response
->withHeader('Access-Control-Allow-Origin', '*')
->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// Conditional responseif ($request->hasHeader('If-None-Match')) {
$etag = $request->getHeaderLine('If-None-Match');
if ($etag === generateEtag($data)) {
$response = $response->withStatus(304); // Not Modified
}
}
$response->send();

API Reference

Request

Factory Methods

Request::capture(): ServerRequestInterface // Capture current request from superglobals

PSR-7 MessageInterface Methods

getProtocolVersion(): string // Get HTTP protocol version (always "1.1" in FastCGI)withProtocolVersion(string $version): static // Return new instance with protocol versiongetHeaders(): array // Get all headers as lowercase key => array valueshasHeader(string $name): bool // Check if header exists (case-insensitive)getHeader(string $name): array // Get header values arraygetHeaderLine(string $name): string // Get header values as comma-separated stringwithHeader(string $name, string|array $value): static // Replace header (case-insensitive)withAddedHeader(string $name, string|array $value): static // Add to existing headerwithoutHeader(string $name): static // Remove headergetBody(): StreamInterface // Get message body streamwithBody(StreamInterface $body): static // Replace body stream

PSR-7 RequestInterface Methods

getRequestTarget(): string // Get request target (path + query)withRequestTarget(string $target): static // Set request targetgetMethod(): string // Get HTTP methodwithMethod(string $method): static // Set HTTP methodgetUri(): string // Get URI as stringwithUri(string|UriInterface $uri, bool $preserveHost = false): static // Set URI

PSR-7 ServerRequestInterface Methods

getServerParams(): array // Get $_SERVER parametersgetCookieParams(): array // Get $_COOKIE parameterswithCookieParams(array $cookies): static // Replace cookiesgetQueryParams(): array // Get $_GET parameterswithQueryParams(array $query): static // Replace query parametersgetUploadedFiles(): array // Get uploaded files structurewithUploadedFiles(array $files): static // Replace uploaded filesgetParsedBody(): array|object|null // Get parsed body (JSON/form data)withParsedBody(array|object|null $data): static // Set parsed bodygetAttributes(): array // Get request attributesgetAttribute(string $name, mixed $default = null) // Get single attributewithAttribute(string $name, mixed $value): static // Add/replace attributewithoutAttribute(string $name): static // Remove attribute

Response

Factory Methods

Response::create(int $status = 200, array $headers = [], mixed $body = null): static
Response::json(mixed $data, int $status = 200): static
Response::text(string $text, int $status = 200): static
Response::html(string $html, int $status = 200): static
Response::redirect(string $url, int $status = 302): static

PSR-7 MessageInterface Methods

getProtocolVersion(): string // Get HTTP protocol versionwithProtocolVersion(string $version): static // Set protocol versiongetHeaders(): array // Get all headershasHeader(string $name): bool // Check header existsgetHeader(string $name): array // Get header valuesgetHeaderLine(string $name): string // Get comma-separated headerwithHeader(string $name, string|array $value): static // Replace headerwithAddedHeader(string $name, string|array $value): static // Add header valuewithoutHeader(string $name): static // Remove headergetBody(): StreamInterface // Get body streamwithBody(StreamInterface $body): static // Replace body stream

PSR-7 ResponseInterface Methods

getStatusCode(): int // Get HTTP status codewithStatus(int $code, string $reason = ''): static // Set status code and reasongetReasonPhrase(): string // Get reason phrase

Output Methods

send(): void // Send response (headers + body)sendHeaders(): void // Send only headerssendBody(): void // Send only body__toString(): string // Serialize to HTTP message

Stream

Factory Methods

Stream::fromString(string $string): static // Create from string (zero-copy)
Stream::fromResource(resource $resource): static // Create from PHP stream resource
Stream::fromFile(string $path, string $mode = 'r'): static // Create from file

PSR-7 StreamInterface Methods

read(int $length): string // Read data from stream
getContents(): string // Get remaining contentswrite(string $string): int // Write data to streamseek(int $offset, int $whence = SEEK_SET): void // Seek to positiontell(): int // Get current positionrewind(): void // Seek to beginningeof(): bool // Check if at end of streamisReadable(): bool // Check if stream is readableisWritable(): bool // Check if stream is writableisSeekable(): bool // Check if stream supports seekinggetSize(): ?int // Get stream size (if known)getMetadata(?string $key = null): mixed // Get stream metadataclose(): void // Close stream and free resourcesdetach(): resource|null // Detach underlying resource__toString(): string // Get entire stream contents

UploadedFile

PSR-7 UploadedFileInterface Methods

getStream(): StreamInterface // Get file contents as streammoveTo(string $targetPath): void // Move file to new locationgetSize(): ?int // Get file size in bytesgetError(): int // Get upload error code (UPLOAD_ERR_*)getClientFilename(): ?string // Get original client filenamegetClientMediaType(): ?string // Get client-provided MIME type

Uri

Factory Methods

Uri::fromString(string $uri): UriInterface // Parse URI string (RFC 3986 compliant)

PSR-7 UriInterface Methods

getScheme(): string // Get URI scheme (http, https, etc.)getAuthority(): string // Get authority (userinfo@host:port)getUserInfo(): string // Get user info (user:pass)getHost(): string // Get host (lowercase)getPort(): ?int // Get port (null if standard for scheme)getPath(): string // Get path componentgetQuery(): string // Get query string (without ?)getFragment(): string // Get fragment (without #)withScheme(string $scheme): UriInterface // Return new instance with schemewithUserInfo(string $user, ?string $pass = null): UriInterface // Set user infowithHost(string $host): UriInterface // Set hostwithPort(?int $port): UriInterface // Set port (null to remove)withPath(string $path): UriInterface // Set pathwithQuery(string $query): UriInterface // Set query stringwithFragment(string $fragment): UriInterface // Set fragment__toString(): string // Serialize to URI string

Performance

The extension provides significant performance improvements over userland PSR-7 implementations through native C code, direct superglobal access, and zero-copy operations. Benchmarks comparing against other PSR-7 implementations can be found in the http-php repository.

Key Optimizations

  • Direct superglobal access - bypasses PHP's array layer for $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
  • Zero-copy string streams - reference strings directly without data duplication
  • Native hash tables - efficient storage and lookup for headers and parameters
  • Lazy evaluation - parse JSON/form data only when accessed
  • Immutable operations - efficient object cloning with shared data structures
  • Memory efficient - proper reference counting and cleanup

How It Works

Request Capture Process

  1. Direct superglobal access - References $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES directly
  2. Lazy header parsing - Headers parsed only when getHeaders() is called
  3. JSON caching - Parsed JSON bodies cached to avoid re-parsing
  4. Immutable cloning - with*() methods create efficient clones with shared data

Stream Operations

  • String streams: TRUE zero-copy references to existing strings (no data duplication)
  • Resource streams: Efficient php_stream_copy_to_mem() for large data
  • Lazy loading: Stream contents read only when accessed
  • Position tracking: Efficient position management for seekable streams

Memory Management

  • Reference counting: Proper Zend reference counting throughout
  • Object pooling: Reuses memory structures where possible
  • Automatic cleanup: Destructors handle resource cleanup
  • Leak prevention: All allocations properly tracked and freed

Structure

http/
├── config.m4 # Build configuration
├── signalforge_http.c # PHP class implementations
├── php_signalforge_http.h # Main header
├── src/
│ ├── request.c/h # Request class implementation
│ ├── response.c/h # Response class implementation
│ ├── stream.c/h # Stream class implementation
│ ├── uri.c/h # Uri class implementation
│ ├── uploadedfile.c/h # UploadedFile class implementation
│ ├── psr7_interfaces.c/h # PSR-7 interface definitions
├── Signalforge/Http/ # IDE stubs
├── examples/ # Usage examples
├── tests/ # phpt test files (97 tests)
└── Dockerfile # Docker build environment

Testing

make test

Or run specific tests:

docker run --rm signalforge-http php /opt/run-tests.php tests/001_basic.phpt

Memory Leak Detection

# Docker-based Valgrind (recommended)
make valgrind-docker
# Local Valgrind (requires valgrind installed)
make valgrind-test

Thread Safety

The extension supports ZTS (Zend Thread Safety) builds. Each request gets isolated instances, and all operations are thread-safe.

Exception Handling

  • InvalidArgumentException - Invalid parameters or malformed data
  • RuntimeException - Stream operations, file access errors
  • Standard PHP exceptions for JSON parsing errors

Related

License

MIT License

About

A native PHP extension implementing a high-performance PSR-7, PSR-17 and PSR-18 compliant HTTP request and response handling - direct HashTable access, zero-copy strings, and lazy evaluation.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Signalforge HTTP Extension

CIPHP 8.3+

A native PHP extension implementing high-performance PSR-7 HTTP Request and Response classes with zero-copy operations and direct superglobal access.

What's Different

  • Native C implementation - all HTTP operations run in native code
  • Zero-copy string streams - reference strings directly without data duplication
  • Direct HashTable access - bypass PHP arrays for superglobal data
  • Lazy evaluation - parse data only when accessed
  • Immutable objects - all with*() methods return new instances
  • Memory efficient - proper reference counting and cleanup
  • PSR-7 compliant - implements ServerRequestInterface, ResponseInterface, StreamInterface, UriInterface, and UploadedFileInterface
  • Optimized for php-fpm - designed for FastCGI environments
  • No dependencies - pure C extension with no external libraries

Why C?

HTTP request/response handling is invoked on nearly every request, often hundreds of times. Moving HTTP operations to native code provides:

  • Direct superglobal access - bypass PHP's array layer for $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
  • Zero-copy string operations - reference string data directly without duplication
  • Native hash tables - efficient storage and lookup for headers and parameters
  • Reduced overhead - minimal PHP engine interaction during data access
  • Memory efficiency - proper reference counting and cleanup
  • Lazy evaluation - parse JSON/form data only when requested
  • Immutable operations - efficient object cloning with shared data structures

Features

  • Full PSR-7 Compliance: Implements ServerRequestInterface, ResponseInterface, StreamInterface, UriInterface, and UploadedFileInterface
  • Zero Dependencies: Pure C extension with no external libraries
  • Hyper-Performance: Direct HashTable access, zero-copy operations, lazy evaluation
  • Immutable Objects: All with*() methods return new instances
  • Memory Efficient: Proper reference counting and cleanup

Streamforge Proxy Integration

The extension integrates seamlessly with the Streamforge FastCGI proxy for high-performance file upload handling. When Streamforge is deployed between nginx and php-fpm, it provides several benefits depending on your nginx configuration.

Understanding nginx Buffering

Important: nginx's fastcgi_request_buffering setting affects what problem streamforge solves:

WITH fastcgi_request_buffering ON (nginx default):
┌────────┐ ┌───────────────┐ ┌─────────┐ ┌───────────┐
│ Client │────▶│ nginx buffers │────▶│ php-fpm │────▶│ $_FILES │
└────────┘ └───────────────┘ └─────────┘ └───────────┘
slow fast │
(to disk) Worker engaged only
during fast transfer
WITH fastcgi_request_buffering OFF:
┌────────┐ ┌───────┐ ┌─────────┐
│ Client │────▶│ nginx │────▶│ php-fpm │ ← Worker blocked for entire upload!
└────────┘ └───────┘ └─────────┘
slow streams
directly

With default nginx settings, workers are already protected from slow clients. The upload is buffered by nginx first.

When Streamforge Helps

nginx settingStreamforge benefit
fastcgi_request_buffering on (default)Avoids double temp file write, reduces PHP memory, skips multipart parsing in PHP
fastcgi_request_buffering offFull benefit: Workers not blocked during slow uploads

With fastcgi_request_buffering off

This is where streamforge shines. Configure nginx to stream directly:

location /upload {fastcgi_request_buffering off; # Stream to backend
fastcgi_pass streamforge:9001;}

Now streamforge handles the slow client I/O:

┌────────┐ ┌─────────────┐ ┌─────────┐
│ Client │────▶│ streamforge │────▶│ php-fpm │
└────────┘ └─────────────┘ └─────────┘
slow writes to disk Worker engaged
as data arrives only ~5ms
ScenarioWithout StreamforgeWith Streamforge
500MB upload, slow clientWorker blocked ~30sWorker engaged ~5ms
20 concurrent uploads, 10 workersSite unresponsiveNo impact

With Default nginx (buffering on)

Streamforge still provides value, just different:

  • No double write: nginx buffer → streamforge disk (not nginx buffer → PHP temp)
  • Less PHP memory: No request body buffering in PHP
  • No multipart parsing: PHP doesn't parse multipart boundaries
  • Consistent API: Same HTTP_X_UPLOAD_* interface regardless of nginx config

Transparent Integration

The extension automatically detects Streamforge and reads uploads from the appropriate source. Your application code remains unchanged:

// Works identically with or without Streamforge$request = Request::capture();
$files = $request->getUploadedFiles();
foreach ($filesas$name => $file) {
$file->getClientFilename(); // "document.pdf"$file->getSize(); // 52428800$file->moveTo('/storage/docs/document.pdf');
}

Detection API

Check if Streamforge is handling the current request:

useSignalforge\NativeHttp\Request;
// Static methodif (Request::isStreamforgeEnabled()) {
// Streamforge is proxying this request
}
// Or check $_SERVER directlyif (isset($_SERVER['HTTP_X_STREAMFORGE'])) {
// Streamforge marker present
}
// Check for processed uploadsif (isset($_SERVER['HTTP_X_UPLOAD_FILE_COUNT'])) {
$count = (int) $_SERVER['HTTP_X_UPLOAD_FILE_COUNT'];
// Streamforge handled $count file uploads
}

Protocol

When Streamforge handles multipart uploads, it:

  1. Parses the multipart body and writes files to disk
  2. Adds metadata headers to the FastCGI request:
    • HTTP_X_STREAMFORGE=1 - Proxy marker
    • HTTP_X_UPLOAD_FILE_COUNT=N - Number of uploaded files
    • HTTP_X_UPLOAD_0_NAME - Form field name
    • HTTP_X_UPLOAD_0_FILENAME - Original client filename
    • HTTP_X_UPLOAD_0_PATH - Temp file path on disk
    • HTTP_X_UPLOAD_0_SIZE - File size in bytes
    • HTTP_X_UPLOAD_0_TYPE - MIME type
  3. Sends only form fields (not file content) to PHP-FPM

The extension reads these headers and creates UploadedFile objects that work identically to standard PHP uploads.

Cleanup

Temp files are automatically cleaned up:

  • On moveTo(): File is moved, no cleanup needed
  • On request end: Unmoved temp files are deleted by the extension's RSHUTDOWN handler

This prevents disk space leaks even if application code doesn't handle all uploaded files.

Deployment

See the Streamforge documentation for deployment instructions. Basic setup:

# Start Streamforge between nginx and php-fpm
streamforge -l 0.0.0.0:9001 -u /var/run/php-fpm.sock -d /tmp/uploads
# Configure nginx to send requests to Streamforge# fastcgi_pass 127.0.0.1:9001;

Requirements

  • PHP 8.3, 8.4, or 8.5
  • Linux or macOS (tested on x86_64 and ARM64)
  • php-fpm recommended (works in CLI for testing)

Building

Docker (Recommended)

No need to install PHP dev headers on your host:

cd http
# Build Docker image with extension
make docker-build
# Run tests
make docker-test
# Test all PHP versions (8.3, 8.4, 8.5)
make ci-test-all
# Run example
make docker-example

Host Installation

cd http
phpize
./configure --enable-signalforge_http
make
make test
sudo make install

Then add extension=signalforge_http.so to your php.ini.

Usage

Request

<?phpuseSignalforge\NativeHttp\Request;
// Capture the current request$request = Request::capture();
// HTTP Method & URI$method = $request->getMethod(); // "POST"$target = $request->getRequestTarget(); // "/users/123?include=profile"$path = $request->getUri(); // "/users/123?include=profile"// Headers (case-insensitive)$contentType = $request->getHeader('Content-Type'); // ['application/json']$contentTypeLine = $request->getHeaderLine('Content-Type'); // "application/json"$hasAuth = $request->hasHeader('Authorization'); // true/false$allHeaders = $request->getHeaders(); // ['content-type' => ['application/json']]// Parameters$queryParams = $request->getQueryParams(); // $_GET as array$parsedBody = $request->getParsedBody(); // JSON/form data (lazy parsed)// Body access$bodyStream = $request->getBody(); // StreamInterface$rawBody = (string) $request->getBody(); // Raw body string// Server & environment$serverParams = $request->getServerParams(); // $_SERVER$userAgent = $serverParams['HTTP_USER_AGENT'];
// Cookies$cookies = $request->getCookieParams(); // $_COOKIE as array$sessionId = $cookies['session_id'];
// Uploaded files$files = $request->getUploadedFiles(); // Normalized file structureif (isset($files['avatar'])) {
$filename = $files['avatar']->getClientFilename();
$files['avatar']->moveTo('/uploads/' . $filename);
}
// Attributes (middleware data)$request = $request->withAttribute('user_id', 123);
$userId = $request->getAttribute('user_id'); // 123$userId = $request->getAttribute('missing', 'default'); // 'default'// Immutable modifications$newRequest = $request
->withMethod('PUT')
->withHeader('X-API-Key', 'secret')
->withQueryParams(['limit' => 10])
->withParsedBody(['name' => 'John']);
// Original request unchangedassert($request->getMethod() === 'POST');
assert($newRequest->getMethod() === 'PUT');

Response

<?phpuseSignalforge\NativeHttp\Response;
useSignalforge\NativeHttp\Stream;
// Factory methods$response = Response::create(200, ['Content-Type' => 'application/json']);
$response = Response::json(['users' => ['id' => 1, 'name' => 'John']], 200);
$response = Response::text('Hello World', 200);
$response = Response::html('<h1>Welcome</h1>', 200);
$response = Response::redirect('/login', 302);
// Status management$statusCode = $response->getStatusCode(); // 200$reasonPhrase = $response->getReasonPhrase(); // "OK"$response = $response->withStatus(404, 'Not Found');
// Header management (case-insensitive)$response = $response->withHeader('Content-Type', 'application/json');
$response = $response->withAddedHeader('Cache-Control', 'no-cache');
$response = $response->withAddedHeader('Cache-Control', 'private');
$hasHeader = $response->hasHeader('Content-Type'); // true$headerValue = $response->getHeader('Content-Type'); // ['application/json']$headerLine = $response->getHeaderLine('Content-Type'); // "application/json"$allHeaders = $response->getHeaders();
// Body management$stream = Stream::fromString('{"message": "Hello"}');
$response = $response->withBody($stream);
$bodyStream = $response->getBody();
// Output$response->send(); // Send headers + body$response->sendHeaders(); // Send only headers$response->sendBody(); // Send only body// Serialization$message = (string) $response; // Full HTTP message

Stream

<?phpuseSignalforge\NativeHttp\Stream;
// Factory methods$stream = Stream::fromString('Hello World'); // TRUE zero-copy string reference$stream = Stream::fromResource(fopen('file.txt', 'r')); // From PHP resource$stream = Stream::fromFile('/path/to/file', 'r'); // From file path// Reading operations$data = $stream->read(5); // Read 5 bytes: "Hello"$remaining = $stream->getContents(); // Get rest: " World"$stream->rewind(); // Reset to beginning$all = (string) $stream; // Get entire contents// Writing operations (use file or resource streams for writing)$writableStream = Stream::fromFile('/tmp/output.txt', 'w+');
$bytesWritten = $writableStream->write('Hello'); // Write data$writableStream->write(' World'); // Append more// Seeking operations$stream->seek(6); // Seek to position 6$position = $stream->tell(); // Get current position: 6$stream->rewind(); // Reset to beginning// Stream capabilities$isReadable = $stream->isReadable(); // Check if can read$isWritable = $stream->isWritable(); // Check if can write$isSeekable = $stream->isSeekable(); // Check if supports seeking$atEnd = $stream->eof(); // Check if at end// Metadata and size$size = $stream->getSize(); // Size in bytes (or null)$metadata = $stream->getMetadata(); // All metadata$uri = $stream->getMetadata('uri'); // Specific metadata key// Resource management$underlying = $stream->detach(); // Detach PHP resource$stream->close(); // Close stream and free resources

Uri

<?phpuseSignalforge\NativeHttp\Uri;
// Parse a URI string$uri = Uri::fromString('https://user:pass@example.com:8080/path?query=value#fragment');
// Access components (PSR-7 UriInterface)$scheme = $uri->getScheme(); // "https"$userInfo = $uri->getUserInfo(); // "user:pass"$host = $uri->getHost(); // "example.com"$port = $uri->getPort(); // 8080 (null if standard port for scheme)$path = $uri->getPath(); // "/path"$query = $uri->getQuery(); // "query=value"$fragment = $uri->getFragment(); // "fragment"$authority = $uri->getAuthority(); // "user:pass@example.com:8080"// Serialize to string$uriString = (string) $uri; // "https://user:pass@example.com:8080/path?query=value#fragment"// Immutable modifications$newUri = $uri
->withScheme('http')
->withHost('api.example.com')
->withPort(null) // Remove explicit port
->withPath('/v2/users')
->withQuery('limit=10')
->withFragment('');
// Original URI unchangedassert($uri->getHost() === 'example.com');
assert($newUri->getHost() === 'api.example.com');
// Standard ports are normalized to null$httpsUri = Uri::fromString('https://example.com:443/path');
$port = $httpsUri->getPort(); // null (443 is standard for https)

UploadedFile

<?phpuseSignalforge\NativeHttp\Request;
// Get uploaded files from request$request = Request::capture();
$files = $request->getUploadedFiles();
// Single file uploadif (isset($files['avatar'])) {
$file = $files['avatar'];
// File properties$size = $file->getSize(); // Size in bytes$error = $file->getError(); // UPLOAD_ERR_* constant$clientName = $file->getClientFilename(); // Original filename$mimeType = $file->getClientMediaType(); // MIME type// Move file to permanent location$targetPath = '/uploads/avatars/' . uniqid() . '_' . $clientName;
$file->moveTo($targetPath);
// Note: moveTo() can only be called once per UploadedFile
}
// Multiple file uploadif (isset($files['photos'])) {
foreach ($files['photos'] as$photo) {
if ($photo->getError() === UPLOAD_ERR_OK) {
$filename = $photo->getClientFilename();
$photo->moveTo('/uploads/photos/' . $filename);
}
}
}
// Stream access (alternative to moveTo)$stream = $file->getStream();
$content = $stream->getContents();

Advanced Patterns

<?phpuseSignalforge\NativeHttp\{Request, Response, Stream};
// Middleware-style request processingfunctionauthenticate(Request$request): Request
{
$token = $request->getHeaderLine('Authorization');
$userId = validateToken($token);
return$request->withAttribute('user_id', $userId);
}
functionvalidateJson(Request$request): Request
{
$contentType = $request->getHeaderLine('Content-Type');
if (!str_contains($contentType, 'application/json')) {
thrownewInvalidArgumentException('JSON content type required');
}
return$request;
}
// Request processing pipeline$request = Request::capture();
$request = authenticate($request);
$request = validateJson($request);
// JSON API response$data = ['users' => getUsers($request->getAttribute('user_id'))];
$response = Response::json($data, 200);
// CORS headers$response = $response
->withHeader('Access-Control-Allow-Origin', '*')
->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// Conditional responseif ($request->hasHeader('If-None-Match')) {
$etag = $request->getHeaderLine('If-None-Match');
if ($etag === generateEtag($data)) {
$response = $response->withStatus(304); // Not Modified
}
}
$response->send();

API Reference

Request

Factory Methods

Request::capture(): ServerRequestInterface // Capture current request from superglobals

PSR-7 MessageInterface Methods

getProtocolVersion(): string // Get HTTP protocol version (always "1.1" in FastCGI)withProtocolVersion(string $version): static // Return new instance with protocol versiongetHeaders(): array // Get all headers as lowercase key => array valueshasHeader(string $name): bool // Check if header exists (case-insensitive)getHeader(string $name): array // Get header values arraygetHeaderLine(string $name): string // Get header values as comma-separated stringwithHeader(string $name, string|array $value): static // Replace header (case-insensitive)withAddedHeader(string $name, string|array $value): static // Add to existing headerwithoutHeader(string $name): static // Remove headergetBody(): StreamInterface // Get message body streamwithBody(StreamInterface $body): static // Replace body stream

PSR-7 RequestInterface Methods

getRequestTarget(): string // Get request target (path + query)withRequestTarget(string $target): static // Set request targetgetMethod(): string // Get HTTP methodwithMethod(string $method): static // Set HTTP methodgetUri(): string // Get URI as stringwithUri(string|UriInterface $uri, bool $preserveHost = false): static // Set URI

PSR-7 ServerRequestInterface Methods

getServerParams(): array // Get $_SERVER parametersgetCookieParams(): array // Get $_COOKIE parameterswithCookieParams(array $cookies): static // Replace cookiesgetQueryParams(): array // Get $_GET parameterswithQueryParams(array $query): static // Replace query parametersgetUploadedFiles(): array // Get uploaded files structurewithUploadedFiles(array $files): static // Replace uploaded filesgetParsedBody(): array|object|null // Get parsed body (JSON/form data)withParsedBody(array|object|null $data): static // Set parsed bodygetAttributes(): array // Get request attributesgetAttribute(string $name, mixed $default = null) // Get single attributewithAttribute(string $name, mixed $value): static // Add/replace attributewithoutAttribute(string $name): static // Remove attribute

Response

Factory Methods

Response::create(int $status = 200, array $headers = [], mixed $body = null): static
Response::json(mixed $data, int $status = 200): static
Response::text(string $text, int $status = 200): static
Response::html(string $html, int $status = 200): static
Response::redirect(string $url, int $status = 302): static

PSR-7 MessageInterface Methods

getProtocolVersion(): string // Get HTTP protocol versionwithProtocolVersion(string $version): static // Set protocol versiongetHeaders(): array // Get all headershasHeader(string $name): bool // Check header existsgetHeader(string $name): array // Get header valuesgetHeaderLine(string $name): string // Get comma-separated headerwithHeader(string $name, string|array $value): static // Replace headerwithAddedHeader(string $name, string|array $value): static // Add header valuewithoutHeader(string $name): static // Remove headergetBody(): StreamInterface // Get body streamwithBody(StreamInterface $body): static // Replace body stream

PSR-7 ResponseInterface Methods

getStatusCode(): int // Get HTTP status codewithStatus(int $code, string $reason = ''): static // Set status code and reasongetReasonPhrase(): string // Get reason phrase

Output Methods

send(): void // Send response (headers + body)sendHeaders(): void // Send only headerssendBody(): void // Send only body__toString(): string // Serialize to HTTP message

Stream

Factory Methods

Stream::fromString(string $string): static // Create from string (zero-copy)
Stream::fromResource(resource $resource): static // Create from PHP stream resource
Stream::fromFile(string $path, string $mode = 'r'): static // Create from file

PSR-7 StreamInterface Methods

read(int $length): string // Read data from stream
getContents(): string // Get remaining contentswrite(string $string): int // Write data to streamseek(int $offset, int $whence = SEEK_SET): void // Seek to positiontell(): int // Get current positionrewind(): void // Seek to beginningeof(): bool // Check if at end of streamisReadable(): bool // Check if stream is readableisWritable(): bool // Check if stream is writableisSeekable(): bool // Check if stream supports seekinggetSize(): ?int // Get stream size (if known)getMetadata(?string $key = null): mixed // Get stream metadataclose(): void // Close stream and free resourcesdetach(): resource|null // Detach underlying resource__toString(): string // Get entire stream contents

UploadedFile

PSR-7 UploadedFileInterface Methods

getStream(): StreamInterface // Get file contents as streammoveTo(string $targetPath): void // Move file to new locationgetSize(): ?int // Get file size in bytesgetError(): int // Get upload error code (UPLOAD_ERR_*)getClientFilename(): ?string // Get original client filenamegetClientMediaType(): ?string // Get client-provided MIME type

Uri

Factory Methods

Uri::fromString(string $uri): UriInterface // Parse URI string (RFC 3986 compliant)

PSR-7 UriInterface Methods

getScheme(): string // Get URI scheme (http, https, etc.)getAuthority(): string // Get authority (userinfo@host:port)getUserInfo(): string // Get user info (user:pass)getHost(): string // Get host (lowercase)getPort(): ?int // Get port (null if standard for scheme)getPath(): string // Get path componentgetQuery(): string // Get query string (without ?)getFragment(): string // Get fragment (without #)withScheme(string $scheme): UriInterface // Return new instance with schemewithUserInfo(string $user, ?string $pass = null): UriInterface // Set user infowithHost(string $host): UriInterface // Set hostwithPort(?int $port): UriInterface // Set port (null to remove)withPath(string $path): UriInterface // Set pathwithQuery(string $query): UriInterface // Set query stringwithFragment(string $fragment): UriInterface // Set fragment__toString(): string // Serialize to URI string

Performance

The extension provides significant performance improvements over userland PSR-7 implementations through native C code, direct superglobal access, and zero-copy operations. Benchmarks comparing against other PSR-7 implementations can be found in the http-php repository.

Key Optimizations

  • Direct superglobal access - bypasses PHP's array layer for $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
  • Zero-copy string streams - reference strings directly without data duplication
  • Native hash tables - efficient storage and lookup for headers and parameters
  • Lazy evaluation - parse JSON/form data only when accessed
  • Immutable operations - efficient object cloning with shared data structures
  • Memory efficient - proper reference counting and cleanup

How It Works

Request Capture Process

  1. Direct superglobal access - References $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES directly
  2. Lazy header parsing - Headers parsed only when getHeaders() is called
  3. JSON caching - Parsed JSON bodies cached to avoid re-parsing
  4. Immutable cloning - with*() methods create efficient clones with shared data

Stream Operations

  • String streams: TRUE zero-copy references to existing strings (no data duplication)
  • Resource streams: Efficient php_stream_copy_to_mem() for large data
  • Lazy loading: Stream contents read only when accessed
  • Position tracking: Efficient position management for seekable streams

Memory Management

  • Reference counting: Proper Zend reference counting throughout
  • Object pooling: Reuses memory structures where possible
  • Automatic cleanup: Destructors handle resource cleanup
  • Leak prevention: All allocations properly tracked and freed

Structure

http/
├── config.m4 # Build configuration
├── signalforge_http.c # PHP class implementations
├── php_signalforge_http.h # Main header
├── src/
│ ├── request.c/h # Request class implementation
│ ├── response.c/h # Response class implementation
│ ├── stream.c/h # Stream class implementation
│ ├── uri.c/h # Uri class implementation
│ ├── uploadedfile.c/h # UploadedFile class implementation
│ ├── psr7_interfaces.c/h # PSR-7 interface definitions
├── Signalforge/Http/ # IDE stubs
├── examples/ # Usage examples
├── tests/ # phpt test files (97 tests)
└── Dockerfile # Docker build environment

Testing

make test

Or run specific tests:

docker run --rm signalforge-http php /opt/run-tests.php tests/001_basic.phpt

Memory Leak Detection

# Docker-based Valgrind (recommended)
make valgrind-docker
# Local Valgrind (requires valgrind installed)
make valgrind-test

Thread Safety

The extension supports ZTS (Zend Thread Safety) builds. Each request gets isolated instances, and all operations are thread-safe.

Exception Handling

  • InvalidArgumentException - Invalid parameters or malformed data
  • RuntimeException - Stream operations, file access errors
  • Standard PHP exceptions for JSON parsing errors

Related

License

MIT License

About

A native PHP extension implementing a high-performance PSR-7, PSR-17 and PSR-18 compliant HTTP request and response handling - direct HashTable access, zero-copy strings, and lazy evaluation.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Signalforge HTTP Extension

CIPHP 8.3+

A native PHP extension implementing high-performance PSR-7 HTTP Request and Response classes with zero-copy operations and direct superglobal access.

What's Different

  • Native C implementation - all HTTP operations run in native code
  • Zero-copy string streams - reference strings directly without data duplication
  • Direct HashTable access - bypass PHP arrays for superglobal data
  • Lazy evaluation - parse data only when accessed
  • Immutable objects - all with*() methods return new instances
  • Memory efficient - proper reference counting and cleanup
  • PSR-7 compliant - implements ServerRequestInterface, ResponseInterface, StreamInterface, UriInterface, and UploadedFileInterface
  • Optimized for php-fpm - designed for FastCGI environments
  • No dependencies - pure C extension with no external libraries

Why C?

HTTP request/response handling is invoked on nearly every request, often hundreds of times. Moving HTTP operations to native code provides:

  • Direct superglobal access - bypass PHP's array layer for $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
  • Zero-copy string operations - reference string data directly without duplication
  • Native hash tables - efficient storage and lookup for headers and parameters
  • Reduced overhead - minimal PHP engine interaction during data access
  • Memory efficiency - proper reference counting and cleanup
  • Lazy evaluation - parse JSON/form data only when requested
  • Immutable operations - efficient object cloning with shared data structures

Features

  • Full PSR-7 Compliance: Implements ServerRequestInterface, ResponseInterface, StreamInterface, UriInterface, and UploadedFileInterface
  • Zero Dependencies: Pure C extension with no external libraries
  • Hyper-Performance: Direct HashTable access, zero-copy operations, lazy evaluation
  • Immutable Objects: All with*() methods return new instances
  • Memory Efficient: Proper reference counting and cleanup

Streamforge Proxy Integration

The extension integrates seamlessly with the Streamforge FastCGI proxy for high-performance file upload handling. When Streamforge is deployed between nginx and php-fpm, it provides several benefits depending on your nginx configuration.

Understanding nginx Buffering

Important: nginx's fastcgi_request_buffering setting affects what problem streamforge solves:

WITH fastcgi_request_buffering ON (nginx default):
┌────────┐ ┌───────────────┐ ┌─────────┐ ┌───────────┐
│ Client │────▶│ nginx buffers │────▶│ php-fpm │────▶│ $_FILES │
└────────┘ └───────────────┘ └─────────┘ └───────────┘
slow fast │
(to disk) Worker engaged only
during fast transfer
WITH fastcgi_request_buffering OFF:
┌────────┐ ┌───────┐ ┌─────────┐
│ Client │────▶│ nginx │────▶│ php-fpm │ ← Worker blocked for entire upload!
└────────┘ └───────┘ └─────────┘
slow streams
directly

With default nginx settings, workers are already protected from slow clients. The upload is buffered by nginx first.

When Streamforge Helps

nginx settingStreamforge benefit
fastcgi_request_buffering on (default)Avoids double temp file write, reduces PHP memory, skips multipart parsing in PHP
fastcgi_request_buffering offFull benefit: Workers not blocked during slow uploads

With fastcgi_request_buffering off

This is where streamforge shines. Configure nginx to stream directly:

location /upload {fastcgi_request_buffering off; # Stream to backend
fastcgi_pass streamforge:9001;}

Now streamforge handles the slow client I/O:

┌────────┐ ┌─────────────┐ ┌─────────┐
│ Client │────▶│ streamforge │────▶│ php-fpm │
└────────┘ └─────────────┘ └─────────┘
slow writes to disk Worker engaged
as data arrives only ~5ms
ScenarioWithout StreamforgeWith Streamforge
500MB upload, slow clientWorker blocked ~30sWorker engaged ~5ms
20 concurrent uploads, 10 workersSite unresponsiveNo impact

With Default nginx (buffering on)

Streamforge still provides value, just different:

  • No double write: nginx buffer → streamforge disk (not nginx buffer → PHP temp)
  • Less PHP memory: No request body buffering in PHP
  • No multipart parsing: PHP doesn't parse multipart boundaries
  • Consistent API: Same HTTP_X_UPLOAD_* interface regardless of nginx config

Transparent Integration

The extension automatically detects Streamforge and reads uploads from the appropriate source. Your application code remains unchanged:

// Works identically with or without Streamforge$request = Request::capture();
$files = $request->getUploadedFiles();
foreach ($filesas$name => $file) {
$file->getClientFilename(); // "document.pdf"$file->getSize(); // 52428800$file->moveTo('/storage/docs/document.pdf');
}

Detection API

Check if Streamforge is handling the current request:

useSignalforge\NativeHttp\Request;
// Static methodif (Request::isStreamforgeEnabled()) {
// Streamforge is proxying this request
}
// Or check $_SERVER directlyif (isset($_SERVER['HTTP_X_STREAMFORGE'])) {
// Streamforge marker present
}
// Check for processed uploadsif (isset($_SERVER['HTTP_X_UPLOAD_FILE_COUNT'])) {
$count = (int) $_SERVER['HTTP_X_UPLOAD_FILE_COUNT'];
// Streamforge handled $count file uploads
}

Protocol

When Streamforge handles multipart uploads, it:

  1. Parses the multipart body and writes files to disk
  2. Adds metadata headers to the FastCGI request:
    • HTTP_X_STREAMFORGE=1 - Proxy marker
    • HTTP_X_UPLOAD_FILE_COUNT=N - Number of uploaded files
    • HTTP_X_UPLOAD_0_NAME - Form field name
    • HTTP_X_UPLOAD_0_FILENAME - Original client filename
    • HTTP_X_UPLOAD_0_PATH - Temp file path on disk
    • HTTP_X_UPLOAD_0_SIZE - File size in bytes
    • HTTP_X_UPLOAD_0_TYPE - MIME type
  3. Sends only form fields (not file content) to PHP-FPM

The extension reads these headers and creates UploadedFile objects that work identically to standard PHP uploads.

Cleanup

Temp files are automatically cleaned up:

  • On moveTo(): File is moved, no cleanup needed
  • On request end: Unmoved temp files are deleted by the extension's RSHUTDOWN handler

This prevents disk space leaks even if application code doesn't handle all uploaded files.

Deployment

See the Streamforge documentation for deployment instructions. Basic setup:

# Start Streamforge between nginx and php-fpm
streamforge -l 0.0.0.0:9001 -u /var/run/php-fpm.sock -d /tmp/uploads
# Configure nginx to send requests to Streamforge# fastcgi_pass 127.0.0.1:9001;

Requirements

  • PHP 8.3, 8.4, or 8.5
  • Linux or macOS (tested on x86_64 and ARM64)
  • php-fpm recommended (works in CLI for testing)

Building

Docker (Recommended)

No need to install PHP dev headers on your host:

cd http
# Build Docker image with extension
make docker-build
# Run tests
make docker-test
# Test all PHP versions (8.3, 8.4, 8.5)
make ci-test-all
# Run example
make docker-example

Host Installation

cd http
phpize
./configure --enable-signalforge_http
make
make test
sudo make install

Then add extension=signalforge_http.so to your php.ini.

Usage

Request

<?phpuseSignalforge\NativeHttp\Request;
// Capture the current request$request = Request::capture();
// HTTP Method & URI$method = $request->getMethod(); // "POST"$target = $request->getRequestTarget(); // "/users/123?include=profile"$path = $request->getUri(); // "/users/123?include=profile"// Headers (case-insensitive)$contentType = $request->getHeader('Content-Type'); // ['application/json']$contentTypeLine = $request->getHeaderLine('Content-Type'); // "application/json"$hasAuth = $request->hasHeader('Authorization'); // true/false$allHeaders = $request->getHeaders(); // ['content-type' => ['application/json']]// Parameters$queryParams = $request->getQueryParams(); // $_GET as array$parsedBody = $request->getParsedBody(); // JSON/form data (lazy parsed)// Body access$bodyStream = $request->getBody(); // StreamInterface$rawBody = (string) $request->getBody(); // Raw body string// Server & environment$serverParams = $request->getServerParams(); // $_SERVER$userAgent = $serverParams['HTTP_USER_AGENT'];
// Cookies$cookies = $request->getCookieParams(); // $_COOKIE as array$sessionId = $cookies['session_id'];
// Uploaded files$files = $request->getUploadedFiles(); // Normalized file structureif (isset($files['avatar'])) {
$filename = $files['avatar']->getClientFilename();
$files['avatar']->moveTo('/uploads/' . $filename);
}
// Attributes (middleware data)$request = $request->withAttribute('user_id', 123);
$userId = $request->getAttribute('user_id'); // 123$userId = $request->getAttribute('missing', 'default'); // 'default'// Immutable modifications$newRequest = $request
->withMethod('PUT')
->withHeader('X-API-Key', 'secret')
->withQueryParams(['limit' => 10])
->withParsedBody(['name' => 'John']);
// Original request unchangedassert($request->getMethod() === 'POST');
assert($newRequest->getMethod() === 'PUT');

Response

<?phpuseSignalforge\NativeHttp\Response;
useSignalforge\NativeHttp\Stream;
// Factory methods$response = Response::create(200, ['Content-Type' => 'application/json']);
$response = Response::json(['users' => ['id' => 1, 'name' => 'John']], 200);
$response = Response::text('Hello World', 200);
$response = Response::html('<h1>Welcome</h1>', 200);
$response = Response::redirect('/login', 302);
// Status management$statusCode = $response->getStatusCode(); // 200$reasonPhrase = $response->getReasonPhrase(); // "OK"$response = $response->withStatus(404, 'Not Found');
// Header management (case-insensitive)$response = $response->withHeader('Content-Type', 'application/json');
$response = $response->withAddedHeader('Cache-Control', 'no-cache');
$response = $response->withAddedHeader('Cache-Control', 'private');
$hasHeader = $response->hasHeader('Content-Type'); // true$headerValue = $response->getHeader('Content-Type'); // ['application/json']$headerLine = $response->getHeaderLine('Content-Type'); // "application/json"$allHeaders = $response->getHeaders();
// Body management$stream = Stream::fromString('{"message": "Hello"}');
$response = $response->withBody($stream);
$bodyStream = $response->getBody();
// Output$response->send(); // Send headers + body$response->sendHeaders(); // Send only headers$response->sendBody(); // Send only body// Serialization$message = (string) $response; // Full HTTP message

Stream

<?phpuseSignalforge\NativeHttp\Stream;
// Factory methods$stream = Stream::fromString('Hello World'); // TRUE zero-copy string reference$stream = Stream::fromResource(fopen('file.txt', 'r')); // From PHP resource$stream = Stream::fromFile('/path/to/file', 'r'); // From file path// Reading operations$data = $stream->read(5); // Read 5 bytes: "Hello"$remaining = $stream->getContents(); // Get rest: " World"$stream->rewind(); // Reset to beginning$all = (string) $stream; // Get entire contents// Writing operations (use file or resource streams for writing)$writableStream = Stream::fromFile('/tmp/output.txt', 'w+');
$bytesWritten = $writableStream->write('Hello'); // Write data$writableStream->write(' World'); // Append more// Seeking operations$stream->seek(6); // Seek to position 6$position = $stream->tell(); // Get current position: 6$stream->rewind(); // Reset to beginning// Stream capabilities$isReadable = $stream->isReadable(); // Check if can read$isWritable = $stream->isWritable(); // Check if can write$isSeekable = $stream->isSeekable(); // Check if supports seeking$atEnd = $stream->eof(); // Check if at end// Metadata and size$size = $stream->getSize(); // Size in bytes (or null)$metadata = $stream->getMetadata(); // All metadata$uri = $stream->getMetadata('uri'); // Specific metadata key// Resource management$underlying = $stream->detach(); // Detach PHP resource$stream->close(); // Close stream and free resources

Uri

<?phpuseSignalforge\NativeHttp\Uri;
// Parse a URI string$uri = Uri::fromString('https://user:pass@example.com:8080/path?query=value#fragment');
// Access components (PSR-7 UriInterface)$scheme = $uri->getScheme(); // "https"$userInfo = $uri->getUserInfo(); // "user:pass"$host = $uri->getHost(); // "example.com"$port = $uri->getPort(); // 8080 (null if standard port for scheme)$path = $uri->getPath(); // "/path"$query = $uri->getQuery(); // "query=value"$fragment = $uri->getFragment(); // "fragment"$authority = $uri->getAuthority(); // "user:pass@example.com:8080"// Serialize to string$uriString = (string) $uri; // "https://user:pass@example.com:8080/path?query=value#fragment"// Immutable modifications$newUri = $uri
->withScheme('http')
->withHost('api.example.com')
->withPort(null) // Remove explicit port
->withPath('/v2/users')
->withQuery('limit=10')
->withFragment('');
// Original URI unchangedassert($uri->getHost() === 'example.com');
assert($newUri->getHost() === 'api.example.com');
// Standard ports are normalized to null$httpsUri = Uri::fromString('https://example.com:443/path');
$port = $httpsUri->getPort(); // null (443 is standard for https)

UploadedFile

<?phpuseSignalforge\NativeHttp\Request;
// Get uploaded files from request$request = Request::capture();
$files = $request->getUploadedFiles();
// Single file uploadif (isset($files['avatar'])) {
$file = $files['avatar'];
// File properties$size = $file->getSize(); // Size in bytes$error = $file->getError(); // UPLOAD_ERR_* constant$clientName = $file->getClientFilename(); // Original filename$mimeType = $file->getClientMediaType(); // MIME type// Move file to permanent location$targetPath = '/uploads/avatars/' . uniqid() . '_' . $clientName;
$file->moveTo($targetPath);
// Note: moveTo() can only be called once per UploadedFile
}
// Multiple file uploadif (isset($files['photos'])) {
foreach ($files['photos'] as$photo) {
if ($photo->getError() === UPLOAD_ERR_OK) {
$filename = $photo->getClientFilename();
$photo->moveTo('/uploads/photos/' . $filename);
}
}
}
// Stream access (alternative to moveTo)$stream = $file->getStream();
$content = $stream->getContents();

Advanced Patterns

<?phpuseSignalforge\NativeHttp\{Request, Response, Stream};
// Middleware-style request processingfunctionauthenticate(Request$request): Request
{
$token = $request->getHeaderLine('Authorization');
$userId = validateToken($token);
return$request->withAttribute('user_id', $userId);
}
functionvalidateJson(Request$request): Request
{
$contentType = $request->getHeaderLine('Content-Type');
if (!str_contains($contentType, 'application/json')) {
thrownewInvalidArgumentException('JSON content type required');
}
return$request;
}
// Request processing pipeline$request = Request::capture();
$request = authenticate($request);
$request = validateJson($request);
// JSON API response$data = ['users' => getUsers($request->getAttribute('user_id'))];
$response = Response::json($data, 200);
// CORS headers$response = $response
->withHeader('Access-Control-Allow-Origin', '*')
->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// Conditional responseif ($request->hasHeader('If-None-Match')) {
$etag = $request->getHeaderLine('If-None-Match');
if ($etag === generateEtag($data)) {
$response = $response->withStatus(304); // Not Modified
}
}
$response->send();

API Reference

Request

Factory Methods

Request::capture(): ServerRequestInterface // Capture current request from superglobals

PSR-7 MessageInterface Methods

getProtocolVersion(): string // Get HTTP protocol version (always "1.1" in FastCGI)withProtocolVersion(string $version): static // Return new instance with protocol versiongetHeaders(): array // Get all headers as lowercase key => array valueshasHeader(string $name): bool // Check if header exists (case-insensitive)getHeader(string $name): array // Get header values arraygetHeaderLine(string $name): string // Get header values as comma-separated stringwithHeader(string $name, string|array $value): static // Replace header (case-insensitive)withAddedHeader(string $name, string|array $value): static // Add to existing headerwithoutHeader(string $name): static // Remove headergetBody(): StreamInterface // Get message body streamwithBody(StreamInterface $body): static // Replace body stream

PSR-7 RequestInterface Methods

getRequestTarget(): string // Get request target (path + query)withRequestTarget(string $target): static // Set request targetgetMethod(): string // Get HTTP methodwithMethod(string $method): static // Set HTTP methodgetUri(): string // Get URI as stringwithUri(string|UriInterface $uri, bool $preserveHost = false): static // Set URI

PSR-7 ServerRequestInterface Methods

getServerParams(): array // Get $_SERVER parametersgetCookieParams(): array // Get $_COOKIE parameterswithCookieParams(array $cookies): static // Replace cookiesgetQueryParams(): array // Get $_GET parameterswithQueryParams(array $query): static // Replace query parametersgetUploadedFiles(): array // Get uploaded files structurewithUploadedFiles(array $files): static // Replace uploaded filesgetParsedBody(): array|object|null // Get parsed body (JSON/form data)withParsedBody(array|object|null $data): static // Set parsed bodygetAttributes(): array // Get request attributesgetAttribute(string $name, mixed $default = null) // Get single attributewithAttribute(string $name, mixed $value): static // Add/replace attributewithoutAttribute(string $name): static // Remove attribute

Response

Factory Methods

Response::create(int $status = 200, array $headers = [], mixed $body = null): static
Response::json(mixed $data, int $status = 200): static
Response::text(string $text, int $status = 200): static
Response::html(string $html, int $status = 200): static
Response::redirect(string $url, int $status = 302): static

PSR-7 MessageInterface Methods

getProtocolVersion(): string // Get HTTP protocol versionwithProtocolVersion(string $version): static // Set protocol versiongetHeaders(): array // Get all headershasHeader(string $name): bool // Check header existsgetHeader(string $name): array // Get header valuesgetHeaderLine(string $name): string // Get comma-separated headerwithHeader(string $name, string|array $value): static // Replace headerwithAddedHeader(string $name, string|array $value): static // Add header valuewithoutHeader(string $name): static // Remove headergetBody(): StreamInterface // Get body streamwithBody(StreamInterface $body): static // Replace body stream

PSR-7 ResponseInterface Methods

getStatusCode(): int // Get HTTP status codewithStatus(int $code, string $reason = ''): static // Set status code and reasongetReasonPhrase(): string // Get reason phrase

Output Methods

send(): void // Send response (headers + body)sendHeaders(): void // Send only headerssendBody(): void // Send only body__toString(): string // Serialize to HTTP message

Stream

Factory Methods

Stream::fromString(string $string): static // Create from string (zero-copy)
Stream::fromResource(resource $resource): static // Create from PHP stream resource
Stream::fromFile(string $path, string $mode = 'r'): static // Create from file

PSR-7 StreamInterface Methods

read(int $length): string // Read data from stream
getContents(): string // Get remaining contentswrite(string $string): int // Write data to streamseek(int $offset, int $whence = SEEK_SET): void // Seek to positiontell(): int // Get current positionrewind(): void // Seek to beginningeof(): bool // Check if at end of streamisReadable(): bool // Check if stream is readableisWritable(): bool // Check if stream is writableisSeekable(): bool // Check if stream supports seekinggetSize(): ?int // Get stream size (if known)getMetadata(?string $key = null): mixed // Get stream metadataclose(): void // Close stream and free resourcesdetach(): resource|null // Detach underlying resource__toString(): string // Get entire stream contents

UploadedFile

PSR-7 UploadedFileInterface Methods

getStream(): StreamInterface // Get file contents as streammoveTo(string $targetPath): void // Move file to new locationgetSize(): ?int // Get file size in bytesgetError(): int // Get upload error code (UPLOAD_ERR_*)getClientFilename(): ?string // Get original client filenamegetClientMediaType(): ?string // Get client-provided MIME type

Uri

Factory Methods

Uri::fromString(string $uri): UriInterface // Parse URI string (RFC 3986 compliant)

PSR-7 UriInterface Methods

getScheme(): string // Get URI scheme (http, https, etc.)getAuthority(): string // Get authority (userinfo@host:port)getUserInfo(): string // Get user info (user:pass)getHost(): string // Get host (lowercase)getPort(): ?int // Get port (null if standard for scheme)getPath(): string // Get path componentgetQuery(): string // Get query string (without ?)getFragment(): string // Get fragment (without #)withScheme(string $scheme): UriInterface // Return new instance with schemewithUserInfo(string $user, ?string $pass = null): UriInterface // Set user infowithHost(string $host): UriInterface // Set hostwithPort(?int $port): UriInterface // Set port (null to remove)withPath(string $path): UriInterface // Set pathwithQuery(string $query): UriInterface // Set query stringwithFragment(string $fragment): UriInterface // Set fragment__toString(): string // Serialize to URI string

Performance

The extension provides significant performance improvements over userland PSR-7 implementations through native C code, direct superglobal access, and zero-copy operations. Benchmarks comparing against other PSR-7 implementations can be found in the http-php repository.

Key Optimizations

  • Direct superglobal access - bypasses PHP's array layer for $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
  • Zero-copy string streams - reference strings directly without data duplication
  • Native hash tables - efficient storage and lookup for headers and parameters
  • Lazy evaluation - parse JSON/form data only when accessed
  • Immutable operations - efficient object cloning with shared data structures
  • Memory efficient - proper reference counting and cleanup

How It Works

Request Capture Process

  1. Direct superglobal access - References $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES directly
  2. Lazy header parsing - Headers parsed only when getHeaders() is called
  3. JSON caching - Parsed JSON bodies cached to avoid re-parsing
  4. Immutable cloning - with*() methods create efficient clones with shared data

Stream Operations

  • String streams: TRUE zero-copy references to existing strings (no data duplication)
  • Resource streams: Efficient php_stream_copy_to_mem() for large data
  • Lazy loading: Stream contents read only when accessed
  • Position tracking: Efficient position management for seekable streams

Memory Management

  • Reference counting: Proper Zend reference counting throughout
  • Object pooling: Reuses memory structures where possible
  • Automatic cleanup: Destructors handle resource cleanup
  • Leak prevention: All allocations properly tracked and freed

Structure

http/
├── config.m4 # Build configuration
├── signalforge_http.c # PHP class implementations
├── php_signalforge_http.h # Main header
├── src/
│ ├── request.c/h # Request class implementation
│ ├── response.c/h # Response class implementation
│ ├── stream.c/h # Stream class implementation
│ ├── uri.c/h # Uri class implementation
│ ├── uploadedfile.c/h # UploadedFile class implementation
│ ├── psr7_interfaces.c/h # PSR-7 interface definitions
├── Signalforge/Http/ # IDE stubs
├── examples/ # Usage examples
├── tests/ # phpt test files (97 tests)
└── Dockerfile # Docker build environment

Testing

make test

Or run specific tests:

docker run --rm signalforge-http php /opt/run-tests.php tests/001_basic.phpt

Memory Leak Detection

# Docker-based Valgrind (recommended)
make valgrind-docker
# Local Valgrind (requires valgrind installed)
make valgrind-test

Thread Safety

The extension supports ZTS (Zend Thread Safety) builds. Each request gets isolated instances, and all operations are thread-safe.

Exception Handling

  • InvalidArgumentException - Invalid parameters or malformed data
  • RuntimeException - Stream operations, file access errors
  • Standard PHP exceptions for JSON parsing errors

Related

License

MIT License

About

A native PHP extension implementing a high-performance PSR-7, PSR-17 and PSR-18 compliant HTTP request and response handling - direct HashTable access, zero-copy strings, and lazy evaluation.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Signalforge HTTP Extension

CIPHP 8.3+

A native PHP extension implementing high-performance PSR-7 HTTP Request and Response classes with zero-copy operations and direct superglobal access.

What's Different

  • Native C implementation - all HTTP operations run in native code
  • Zero-copy string streams - reference strings directly without data duplication
  • Direct HashTable access - bypass PHP arrays for superglobal data
  • Lazy evaluation - parse data only when accessed
  • Immutable objects - all with*() methods return new instances
  • Memory efficient - proper reference counting and cleanup
  • PSR-7 compliant - implements ServerRequestInterface, ResponseInterface, StreamInterface, UriInterface, and UploadedFileInterface
  • Optimized for php-fpm - designed for FastCGI environments
  • No dependencies - pure C extension with no external libraries

Why C?

HTTP request/response handling is invoked on nearly every request, often hundreds of times. Moving HTTP operations to native code provides:

  • Direct superglobal access - bypass PHP's array layer for $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
  • Zero-copy string operations - reference string data directly without duplication
  • Native hash tables - efficient storage and lookup for headers and parameters
  • Reduced overhead - minimal PHP engine interaction during data access
  • Memory efficiency - proper reference counting and cleanup
  • Lazy evaluation - parse JSON/form data only when requested
  • Immutable operations - efficient object cloning with shared data structures

Features

  • Full PSR-7 Compliance: Implements ServerRequestInterface, ResponseInterface, StreamInterface, UriInterface, and UploadedFileInterface
  • Zero Dependencies: Pure C extension with no external libraries
  • Hyper-Performance: Direct HashTable access, zero-copy operations, lazy evaluation
  • Immutable Objects: All with*() methods return new instances
  • Memory Efficient: Proper reference counting and cleanup

Streamforge Proxy Integration

The extension integrates seamlessly with the Streamforge FastCGI proxy for high-performance file upload handling. When Streamforge is deployed between nginx and php-fpm, it provides several benefits depending on your nginx configuration.

Understanding nginx Buffering

Important: nginx's fastcgi_request_buffering setting affects what problem streamforge solves:

WITH fastcgi_request_buffering ON (nginx default):
┌────────┐ ┌───────────────┐ ┌─────────┐ ┌───────────┐
│ Client │────▶│ nginx buffers │────▶│ php-fpm │────▶│ $_FILES │
└────────┘ └───────────────┘ └─────────┘ └───────────┘
slow fast │
(to disk) Worker engaged only
during fast transfer
WITH fastcgi_request_buffering OFF:
┌────────┐ ┌───────┐ ┌─────────┐
│ Client │────▶│ nginx │────▶│ php-fpm │ ← Worker blocked for entire upload!
└────────┘ └───────┘ └─────────┘
slow streams
directly

With default nginx settings, workers are already protected from slow clients. The upload is buffered by nginx first.

When Streamforge Helps

nginx settingStreamforge benefit
fastcgi_request_buffering on (default)Avoids double temp file write, reduces PHP memory, skips multipart parsing in PHP
fastcgi_request_buffering offFull benefit: Workers not blocked during slow uploads

With fastcgi_request_buffering off

This is where streamforge shines. Configure nginx to stream directly:

location /upload {fastcgi_request_buffering off; # Stream to backend
fastcgi_pass streamforge:9001;}

Now streamforge handles the slow client I/O:

┌────────┐ ┌─────────────┐ ┌─────────┐
│ Client │────▶│ streamforge │────▶│ php-fpm │
└────────┘ └─────────────┘ └─────────┘
slow writes to disk Worker engaged
as data arrives only ~5ms
ScenarioWithout StreamforgeWith Streamforge
500MB upload, slow clientWorker blocked ~30sWorker engaged ~5ms
20 concurrent uploads, 10 workersSite unresponsiveNo impact

With Default nginx (buffering on)

Streamforge still provides value, just different:

  • No double write: nginx buffer → streamforge disk (not nginx buffer → PHP temp)
  • Less PHP memory: No request body buffering in PHP
  • No multipart parsing: PHP doesn't parse multipart boundaries
  • Consistent API: Same HTTP_X_UPLOAD_* interface regardless of nginx config

Transparent Integration

The extension automatically detects Streamforge and reads uploads from the appropriate source. Your application code remains unchanged:

// Works identically with or without Streamforge$request = Request::capture();
$files = $request->getUploadedFiles();
foreach ($filesas$name => $file) {
$file->getClientFilename(); // "document.pdf"$file->getSize(); // 52428800$file->moveTo('/storage/docs/document.pdf');
}

Detection API

Check if Streamforge is handling the current request:

useSignalforge\NativeHttp\Request;
// Static methodif (Request::isStreamforgeEnabled()) {
// Streamforge is proxying this request
}
// Or check $_SERVER directlyif (isset($_SERVER['HTTP_X_STREAMFORGE'])) {
// Streamforge marker present
}
// Check for processed uploadsif (isset($_SERVER['HTTP_X_UPLOAD_FILE_COUNT'])) {
$count = (int) $_SERVER['HTTP_X_UPLOAD_FILE_COUNT'];
// Streamforge handled $count file uploads
}

Protocol

When Streamforge handles multipart uploads, it:

  1. Parses the multipart body and writes files to disk
  2. Adds metadata headers to the FastCGI request:
    • HTTP_X_STREAMFORGE=1 - Proxy marker
    • HTTP_X_UPLOAD_FILE_COUNT=N - Number of uploaded files
    • HTTP_X_UPLOAD_0_NAME - Form field name
    • HTTP_X_UPLOAD_0_FILENAME - Original client filename
    • HTTP_X_UPLOAD_0_PATH - Temp file path on disk
    • HTTP_X_UPLOAD_0_SIZE - File size in bytes
    • HTTP_X_UPLOAD_0_TYPE - MIME type
  3. Sends only form fields (not file content) to PHP-FPM

The extension reads these headers and creates UploadedFile objects that work identically to standard PHP uploads.

Cleanup

Temp files are automatically cleaned up:

  • On moveTo(): File is moved, no cleanup needed
  • On request end: Unmoved temp files are deleted by the extension's RSHUTDOWN handler

This prevents disk space leaks even if application code doesn't handle all uploaded files.

Deployment

See the Streamforge documentation for deployment instructions. Basic setup:

# Start Streamforge between nginx and php-fpm
streamforge -l 0.0.0.0:9001 -u /var/run/php-fpm.sock -d /tmp/uploads
# Configure nginx to send requests to Streamforge# fastcgi_pass 127.0.0.1:9001;

Requirements

  • PHP 8.3, 8.4, or 8.5
  • Linux or macOS (tested on x86_64 and ARM64)
  • php-fpm recommended (works in CLI for testing)

Building

Docker (Recommended)

No need to install PHP dev headers on your host:

cd http
# Build Docker image with extension
make docker-build
# Run tests
make docker-test
# Test all PHP versions (8.3, 8.4, 8.5)
make ci-test-all
# Run example
make docker-example

Host Installation

cd http
phpize
./configure --enable-signalforge_http
make
make test
sudo make install

Then add extension=signalforge_http.so to your php.ini.

Usage

Request

<?phpuseSignalforge\NativeHttp\Request;
// Capture the current request$request = Request::capture();
// HTTP Method & URI$method = $request->getMethod(); // "POST"$target = $request->getRequestTarget(); // "/users/123?include=profile"$path = $request->getUri(); // "/users/123?include=profile"// Headers (case-insensitive)$contentType = $request->getHeader('Content-Type'); // ['application/json']$contentTypeLine = $request->getHeaderLine('Content-Type'); // "application/json"$hasAuth = $request->hasHeader('Authorization'); // true/false$allHeaders = $request->getHeaders(); // ['content-type' => ['application/json']]// Parameters$queryParams = $request->getQueryParams(); // $_GET as array$parsedBody = $request->getParsedBody(); // JSON/form data (lazy parsed)// Body access$bodyStream = $request->getBody(); // StreamInterface$rawBody = (string) $request->getBody(); // Raw body string// Server & environment$serverParams = $request->getServerParams(); // $_SERVER$userAgent = $serverParams['HTTP_USER_AGENT'];
// Cookies$cookies = $request->getCookieParams(); // $_COOKIE as array$sessionId = $cookies['session_id'];
// Uploaded files$files = $request->getUploadedFiles(); // Normalized file structureif (isset($files['avatar'])) {
$filename = $files['avatar']->getClientFilename();
$files['avatar']->moveTo('/uploads/' . $filename);
}
// Attributes (middleware data)$request = $request->withAttribute('user_id', 123);
$userId = $request->getAttribute('user_id'); // 123$userId = $request->getAttribute('missing', 'default'); // 'default'// Immutable modifications$newRequest = $request
->withMethod('PUT')
->withHeader('X-API-Key', 'secret')
->withQueryParams(['limit' => 10])
->withParsedBody(['name' => 'John']);
// Original request unchangedassert($request->getMethod() === 'POST');
assert($newRequest->getMethod() === 'PUT');

Response

<?phpuseSignalforge\NativeHttp\Response;
useSignalforge\NativeHttp\Stream;
// Factory methods$response = Response::create(200, ['Content-Type' => 'application/json']);
$response = Response::json(['users' => ['id' => 1, 'name' => 'John']], 200);
$response = Response::text('Hello World', 200);
$response = Response::html('<h1>Welcome</h1>', 200);
$response = Response::redirect('/login', 302);
// Status management$statusCode = $response->getStatusCode(); // 200$reasonPhrase = $response->getReasonPhrase(); // "OK"$response = $response->withStatus(404, 'Not Found');
// Header management (case-insensitive)$response = $response->withHeader('Content-Type', 'application/json');
$response = $response->withAddedHeader('Cache-Control', 'no-cache');
$response = $response->withAddedHeader('Cache-Control', 'private');
$hasHeader = $response->hasHeader('Content-Type'); // true$headerValue = $response->getHeader('Content-Type'); // ['application/json']$headerLine = $response->getHeaderLine('Content-Type'); // "application/json"$allHeaders = $response->getHeaders();
// Body management$stream = Stream::fromString('{"message": "Hello"}');
$response = $response->withBody($stream);
$bodyStream = $response->getBody();
// Output$response->send(); // Send headers + body$response->sendHeaders(); // Send only headers$response->sendBody(); // Send only body// Serialization$message = (string) $response; // Full HTTP message

Stream

<?phpuseSignalforge\NativeHttp\Stream;
// Factory methods$stream = Stream::fromString('Hello World'); // TRUE zero-copy string reference$stream = Stream::fromResource(fopen('file.txt', 'r')); // From PHP resource$stream = Stream::fromFile('/path/to/file', 'r'); // From file path// Reading operations$data = $stream->read(5); // Read 5 bytes: "Hello"$remaining = $stream->getContents(); // Get rest: " World"$stream->rewind(); // Reset to beginning$all = (string) $stream; // Get entire contents// Writing operations (use file or resource streams for writing)$writableStream = Stream::fromFile('/tmp/output.txt', 'w+');
$bytesWritten = $writableStream->write('Hello'); // Write data$writableStream->write(' World'); // Append more// Seeking operations$stream->seek(6); // Seek to position 6$position = $stream->tell(); // Get current position: 6$stream->rewind(); // Reset to beginning// Stream capabilities$isReadable = $stream->isReadable(); // Check if can read$isWritable = $stream->isWritable(); // Check if can write$isSeekable = $stream->isSeekable(); // Check if supports seeking$atEnd = $stream->eof(); // Check if at end// Metadata and size$size = $stream->getSize(); // Size in bytes (or null)$metadata = $stream->getMetadata(); // All metadata$uri = $stream->getMetadata('uri'); // Specific metadata key// Resource management$underlying = $stream->detach(); // Detach PHP resource$stream->close(); // Close stream and free resources

Uri

<?phpuseSignalforge\NativeHttp\Uri;
// Parse a URI string$uri = Uri::fromString('https://user:pass@example.com:8080/path?query=value#fragment');
// Access components (PSR-7 UriInterface)$scheme = $uri->getScheme(); // "https"$userInfo = $uri->getUserInfo(); // "user:pass"$host = $uri->getHost(); // "example.com"$port = $uri->getPort(); // 8080 (null if standard port for scheme)$path = $uri->getPath(); // "/path"$query = $uri->getQuery(); // "query=value"$fragment = $uri->getFragment(); // "fragment"$authority = $uri->getAuthority(); // "user:pass@example.com:8080"// Serialize to string$uriString = (string) $uri; // "https://user:pass@example.com:8080/path?query=value#fragment"// Immutable modifications$newUri = $uri
->withScheme('http')
->withHost('api.example.com')
->withPort(null) // Remove explicit port
->withPath('/v2/users')
->withQuery('limit=10')
->withFragment('');
// Original URI unchangedassert($uri->getHost() === 'example.com');
assert($newUri->getHost() === 'api.example.com');
// Standard ports are normalized to null$httpsUri = Uri::fromString('https://example.com:443/path');
$port = $httpsUri->getPort(); // null (443 is standard for https)

UploadedFile

<?phpuseSignalforge\NativeHttp\Request;
// Get uploaded files from request$request = Request::capture();
$files = $request->getUploadedFiles();
// Single file uploadif (isset($files['avatar'])) {
$file = $files['avatar'];
// File properties$size = $file->getSize(); // Size in bytes$error = $file->getError(); // UPLOAD_ERR_* constant$clientName = $file->getClientFilename(); // Original filename$mimeType = $file->getClientMediaType(); // MIME type// Move file to permanent location$targetPath = '/uploads/avatars/' . uniqid() . '_' . $clientName;
$file->moveTo($targetPath);
// Note: moveTo() can only be called once per UploadedFile
}
// Multiple file uploadif (isset($files['photos'])) {
foreach ($files['photos'] as$photo) {
if ($photo->getError() === UPLOAD_ERR_OK) {
$filename = $photo->getClientFilename();
$photo->moveTo('/uploads/photos/' . $filename);
}
}
}
// Stream access (alternative to moveTo)$stream = $file->getStream();
$content = $stream->getContents();

Advanced Patterns

<?phpuseSignalforge\NativeHttp\{Request, Response, Stream};
// Middleware-style request processingfunctionauthenticate(Request$request): Request
{
$token = $request->getHeaderLine('Authorization');
$userId = validateToken($token);
return$request->withAttribute('user_id', $userId);
}
functionvalidateJson(Request$request): Request
{
$contentType = $request->getHeaderLine('Content-Type');
if (!str_contains($contentType, 'application/json')) {
thrownewInvalidArgumentException('JSON content type required');
}
return$request;
}
// Request processing pipeline$request = Request::capture();
$request = authenticate($request);
$request = validateJson($request);
// JSON API response$data = ['users' => getUsers($request->getAttribute('user_id'))];
$response = Response::json($data, 200);
// CORS headers$response = $response
->withHeader('Access-Control-Allow-Origin', '*')
->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// Conditional responseif ($request->hasHeader('If-None-Match')) {
$etag = $request->getHeaderLine('If-None-Match');
if ($etag === generateEtag($data)) {
$response = $response->withStatus(304); // Not Modified
}
}
$response->send();

API Reference

Request

Factory Methods

Request::capture(): ServerRequestInterface // Capture current request from superglobals

PSR-7 MessageInterface Methods

getProtocolVersion(): string // Get HTTP protocol version (always "1.1" in FastCGI)withProtocolVersion(string $version): static // Return new instance with protocol versiongetHeaders(): array // Get all headers as lowercase key => array valueshasHeader(string $name): bool // Check if header exists (case-insensitive)getHeader(string $name): array // Get header values arraygetHeaderLine(string $name): string // Get header values as comma-separated stringwithHeader(string $name, string|array $value): static // Replace header (case-insensitive)withAddedHeader(string $name, string|array $value): static // Add to existing headerwithoutHeader(string $name): static // Remove headergetBody(): StreamInterface // Get message body streamwithBody(StreamInterface $body): static // Replace body stream

PSR-7 RequestInterface Methods

getRequestTarget(): string // Get request target (path + query)withRequestTarget(string $target): static // Set request targetgetMethod(): string // Get HTTP methodwithMethod(string $method): static // Set HTTP methodgetUri(): string // Get URI as stringwithUri(string|UriInterface $uri, bool $preserveHost = false): static // Set URI

PSR-7 ServerRequestInterface Methods

getServerParams(): array // Get $_SERVER parametersgetCookieParams(): array // Get $_COOKIE parameterswithCookieParams(array $cookies): static // Replace cookiesgetQueryParams(): array // Get $_GET parameterswithQueryParams(array $query): static // Replace query parametersgetUploadedFiles(): array // Get uploaded files structurewithUploadedFiles(array $files): static // Replace uploaded filesgetParsedBody(): array|object|null // Get parsed body (JSON/form data)withParsedBody(array|object|null $data): static // Set parsed bodygetAttributes(): array // Get request attributesgetAttribute(string $name, mixed $default = null) // Get single attributewithAttribute(string $name, mixed $value): static // Add/replace attributewithoutAttribute(string $name): static // Remove attribute

Response

Factory Methods

Response::create(int $status = 200, array $headers = [], mixed $body = null): static
Response::json(mixed $data, int $status = 200): static
Response::text(string $text, int $status = 200): static
Response::html(string $html, int $status = 200): static
Response::redirect(string $url, int $status = 302): static

PSR-7 MessageInterface Methods

getProtocolVersion(): string // Get HTTP protocol versionwithProtocolVersion(string $version): static // Set protocol versiongetHeaders(): array // Get all headershasHeader(string $name): bool // Check header existsgetHeader(string $name): array // Get header valuesgetHeaderLine(string $name): string // Get comma-separated headerwithHeader(string $name, string|array $value): static // Replace headerwithAddedHeader(string $name, string|array $value): static // Add header valuewithoutHeader(string $name): static // Remove headergetBody(): StreamInterface // Get body streamwithBody(StreamInterface $body): static // Replace body stream

PSR-7 ResponseInterface Methods

getStatusCode(): int // Get HTTP status codewithStatus(int $code, string $reason = ''): static // Set status code and reasongetReasonPhrase(): string // Get reason phrase

Output Methods

send(): void // Send response (headers + body)sendHeaders(): void // Send only headerssendBody(): void // Send only body__toString(): string // Serialize to HTTP message

Stream

Factory Methods

Stream::fromString(string $string): static // Create from string (zero-copy)
Stream::fromResource(resource $resource): static // Create from PHP stream resource
Stream::fromFile(string $path, string $mode = 'r'): static // Create from file

PSR-7 StreamInterface Methods

read(int $length): string // Read data from stream
getContents(): string // Get remaining contentswrite(string $string): int // Write data to streamseek(int $offset, int $whence = SEEK_SET): void // Seek to positiontell(): int // Get current positionrewind(): void // Seek to beginningeof(): bool // Check if at end of streamisReadable(): bool // Check if stream is readableisWritable(): bool // Check if stream is writableisSeekable(): bool // Check if stream supports seekinggetSize(): ?int // Get stream size (if known)getMetadata(?string $key = null): mixed // Get stream metadataclose(): void // Close stream and free resourcesdetach(): resource|null // Detach underlying resource__toString(): string // Get entire stream contents

UploadedFile

PSR-7 UploadedFileInterface Methods

getStream(): StreamInterface // Get file contents as streammoveTo(string $targetPath): void // Move file to new locationgetSize(): ?int // Get file size in bytesgetError(): int // Get upload error code (UPLOAD_ERR_*)getClientFilename(): ?string // Get original client filenamegetClientMediaType(): ?string // Get client-provided MIME type

Uri

Factory Methods

Uri::fromString(string $uri): UriInterface // Parse URI string (RFC 3986 compliant)

PSR-7 UriInterface Methods

getScheme(): string // Get URI scheme (http, https, etc.)getAuthority(): string // Get authority (userinfo@host:port)getUserInfo(): string // Get user info (user:pass)getHost(): string // Get host (lowercase)getPort(): ?int // Get port (null if standard for scheme)getPath(): string // Get path componentgetQuery(): string // Get query string (without ?)getFragment(): string // Get fragment (without #)withScheme(string $scheme): UriInterface // Return new instance with schemewithUserInfo(string $user, ?string $pass = null): UriInterface // Set user infowithHost(string $host): UriInterface // Set hostwithPort(?int $port): UriInterface // Set port (null to remove)withPath(string $path): UriInterface // Set pathwithQuery(string $query): UriInterface // Set query stringwithFragment(string $fragment): UriInterface // Set fragment__toString(): string // Serialize to URI string

Performance

The extension provides significant performance improvements over userland PSR-7 implementations through native C code, direct superglobal access, and zero-copy operations. Benchmarks comparing against other PSR-7 implementations can be found in the http-php repository.

Key Optimizations

  • Direct superglobal access - bypasses PHP's array layer for $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
  • Zero-copy string streams - reference strings directly without data duplication
  • Native hash tables - efficient storage and lookup for headers and parameters
  • Lazy evaluation - parse JSON/form data only when accessed
  • Immutable operations - efficient object cloning with shared data structures
  • Memory efficient - proper reference counting and cleanup

How It Works

Request Capture Process

  1. Direct superglobal access - References $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES directly
  2. Lazy header parsing - Headers parsed only when getHeaders() is called
  3. JSON caching - Parsed JSON bodies cached to avoid re-parsing
  4. Immutable cloning - with*() methods create efficient clones with shared data

Stream Operations

  • String streams: TRUE zero-copy references to existing strings (no data duplication)
  • Resource streams: Efficient php_stream_copy_to_mem() for large data
  • Lazy loading: Stream contents read only when accessed
  • Position tracking: Efficient position management for seekable streams

Memory Management

  • Reference counting: Proper Zend reference counting throughout
  • Object pooling: Reuses memory structures where possible
  • Automatic cleanup: Destructors handle resource cleanup
  • Leak prevention: All allocations properly tracked and freed

Structure

http/
├── config.m4 # Build configuration
├── signalforge_http.c # PHP class implementations
├── php_signalforge_http.h # Main header
├── src/
│ ├── request.c/h # Request class implementation
│ ├── response.c/h # Response class implementation
│ ├── stream.c/h # Stream class implementation
│ ├── uri.c/h # Uri class implementation
│ ├── uploadedfile.c/h # UploadedFile class implementation
│ ├── psr7_interfaces.c/h # PSR-7 interface definitions
├── Signalforge/Http/ # IDE stubs
├── examples/ # Usage examples
├── tests/ # phpt test files (97 tests)
└── Dockerfile # Docker build environment

Testing

make test

Or run specific tests:

docker run --rm signalforge-http php /opt/run-tests.php tests/001_basic.phpt

Memory Leak Detection

# Docker-based Valgrind (recommended)
make valgrind-docker
# Local Valgrind (requires valgrind installed)
make valgrind-test

Thread Safety

The extension supports ZTS (Zend Thread Safety) builds. Each request gets isolated instances, and all operations are thread-safe.

Exception Handling

  • InvalidArgumentException - Invalid parameters or malformed data
  • RuntimeException - Stream operations, file access errors
  • Standard PHP exceptions for JSON parsing errors

Related

License

MIT License

About

A native PHP extension implementing a high-performance PSR-7, PSR-17 and PSR-18 compliant HTTP request and response handling - direct HashTable access, zero-copy strings, and lazy evaluation.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages