semperton / proxy
Simple PSR-18 HTTP client based on PHP's native stream socket.
Requires
- php: ^8.1
- psr/http-client: ^1.0.3
- psr/http-message: ^1.1 || ^2.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.95
- httpsoft/http-message: ^1.1
- phpunit/phpunit: ^10.5 || ^11.5 || ^12.5
- vimeo/psalm: ^6.20
Suggests
- ext-zlib: Needed to decode gzip and deflate compressed responses
Provides
Conflicts
None
Replaces
None
README
Semperton Proxy
Simple PSR-18 HTTP client based on PHP's native stream socket.
Installation
Just use Composer:
composer require semperton/proxy
Proxy requires PHP 8.1+ (and the openssl extension for HTTPS).
Usage
The client does not come with a PSR-17 response factory by itself. You have to provide one in the constructor.
use HttpSoft\Message\ResponseFactory; use Semperton\Proxy\Client; $client = new Client( new ResponseFactory(), // any PSR-17 compliant response factory timeout: 5, // seconds to wait for the connection or the server (idle timeout) contextOptions: $options, // array of stream context options bufferSize: 8192, // bytes to read/write at once maxDuration: 30, // seconds the whole exchange may take, null for no limit unixSocket: null, // path of a Unix domain socket to connect to instead decodeContent: false, // request and decode gzip/deflate responses );
All parameters except the response factory are optional.
The context options are passed to stream_context_create.
For example, use the ssl options to configure certificate verification, or crypto_method to choose the TLS version:
$options = [ 'ssl' => [ 'cafile' => '/path/to/ca.pem', 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT, ], ];
The client exposes only one public method, sendRequest:
use HttpSoft\Message\RequestFactory; use Psr\Http\Message\ResponseInterface; $requestFactory = new RequestFactory(); $request = $requestFactory->createRequest('GET', 'https://example.com'); $response = $client->sendRequest($request); $response instanceof ResponseInterface; // true
The response body is read from the socket on demand, so large responses can be streamed:
$body = $response->getBody(); while (!$body->eof()) { echo $body->read(8192); }
Timeouts
There are two independent limits:
timeoutis an idle timeout. It applies to connecting and to every single read or write, so a large download never fails as long as data keeps arriving.maxDurationlimits the whole exchange: connecting, sending the request and reading the response, including the body. It protects against servers that send data very slowly. When it is exceeded while reading the body, the body read throws aRuntimeException.
// fail if the server stalls for 5 seconds, or if everything takes longer than 30 seconds $client = new Client(new ResponseFactory(), timeout: 5, maxDuration: 30);
Unix domain sockets
Set unixSocket to talk to a local service such as the Docker daemon. The host of the request URI is only used for the Host header:
$client = new Client(new ResponseFactory(), unixSocket: '/var/run/docker.sock'); $request = $requestFactory->createRequest('GET', 'http://localhost/version');
TLS over Unix domain sockets is not supported.
Compression
With decodeContent: true the client sends Accept-Encoding: gzip, deflate and decodes such responses while they are read. The Content-Encoding and Content-Length headers are removed from decoded responses, as PSR-18 requires. This needs the zlib extension.
If the request already has an Accept-Encoding header, it is kept. Only gzip and deflate responses are decoded, other codings (e.g. br) are left untouched.
Errors
Responses with 4xx or 5xx status codes are returned as normal responses. Exceptions are only thrown when no valid response could be received:
Semperton\Proxy\Exception\RequestException(Psr\Http\Client\RequestExceptionInterface) when the request is invalid or cannot be sent by this client. Examples are an unsupported protocol version, aCONNECTrequest, or a body that cannot be read.Semperton\Proxy\Exception\NetworkException(Psr\Http\Client\NetworkExceptionInterface) when the connection fails, times out, or the response is malformed.
Both extend Semperton\Proxy\Exception\ClientException, and getRequest() returns the request that was sent.
Reading the response body throws a RuntimeException if the body is incomplete, for example when the connection is closed early or a read times out.
Features and limits
- HTTP/1.0 and HTTP/1.1 over plain TCP or TLS
- Chunked response bodies are decoded.
gzipanddeflatecontent can be decoded on request, see Compression - Request bodies of unknown size are buffered to send a
Content-Length. Set aTransfer-Encoding: chunkedheader on the request to stream them chunked instead - Interim
1xxresponses are skipped - One connection per request (
Connection: close), no redirects, no proxies, no protocol upgrades - Response heads are limited to 256 KiB and 10 interim responses, so a hostile server cannot exhaust memory, and
maxDurationlimits the total time
This is just a simple HTTP client for single requests. If you are going to do heavy API work (concurrent requests, connection reuse, retries, etc.), you should consider using guzzlehttp/guzzle or symfony/http-client.
Versioning
This library follows semantic versioning. The public API is the Client class, including the names of its constructor parameters since they can be used as named arguments, and the classes in Semperton\Proxy\Exception.
Everything in Semperton\Proxy\Internal is an implementation detail and may change in any release.
Development
composer check # runs all of the following
composer cs # coding style (composer cs:fix to fix)
composer psalm # static analysis
composer test # offline test suite (includes TLS against a local server)
vendor/bin/phpunit --group network # tests against httpbin.org