rasuvaeff / context-http
PSR HTTP adapters for rasuvaeff/context
Requires
- php: 8.3 - 8.5
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^2.0
- psr/http-server-handler: ^1.0
- psr/http-server-middleware: ^1.0
- rasuvaeff/context: ^0.2
Requires (Dev)
- ergebnis/composer-normalize: ^2.51
- friendsofphp/php-cs-fixer: ^3.95
- guzzlehttp/guzzle: ^7.9
- infection/infection: ^0.35
- maglnet/composer-require-checker: ^4.17
- nyholm/psr7: ^1.8
- rasuvaeff/property-testing-testo: ^1.0
- rasuvaeff/rector-named-literals: ^1.0
- rector/rector: ^2.4
- roave/backward-compatibility-check: ^8.0
- testo/bridge-infection: ^0.1.6
- testo/testo: ^0.10.25
- vimeo/psalm: ^6.16
Suggests
- guzzlehttp/guzzle: Enables ContextClient::send() with per-request timeouts
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-11 09:38:15 UTC
README
PSR-15 and PSR-18 adapters for rasuvaeff/context:
a request-scoped deadline, cancellation and correlation id, propagated across service calls.
Using an AI coding assistant? llms.txt contains a compact API reference you can share with the model.
Requirements
- PHP 8.3+
rasuvaeff/context^0.2psr/http-message^2.0,psr/http-factory^1.0,psr/http-client^1.0,psr/http-server-handler^1.0,psr/http-server-middleware^1.0guzzlehttp/guzzle^7 only forContextClient(optional)
Installation
composer require rasuvaeff/context-http
Usage
ContextMiddleware (PSR-15)
Creates one context per request, publishes it as request attributes and
cancels it in finally:
use Rasuvaeff\Context\Context; use Rasuvaeff\Context\ContextController; use Rasuvaeff\Context\Http\ContextMiddleware; use Rasuvaeff\Context\Http\CorrelationId; $middleware = new ContextMiddleware( timeout: 5.0, // seconds, or fn (ServerRequestInterface): float deadlineHeader: 'X-Request-Timeout-Ms', // optional: accept an upstream budget responseFactory: $psr17Factory, // optional: map context exceptions to 504/499 ); // in the handler $context = $request->getAttribute(Context::class); $controller = $request->getAttribute(ContextController::class); $correlationId = $request->getAttribute(CorrelationId::class); // always set
| Argument | Default | Effect |
|---|---|---|
timeout |
30.0 |
Request budget in seconds, or a callable deriving it from the request |
correlationHeader |
X-Correlation-ID |
Incoming correlation header |
contextAttribute / controllerAttribute / correlationAttribute |
class names | Request attribute names |
timeSource |
HrTimeSource |
Monotonic clock (tests) |
deadlineHeader |
null |
Incoming budget in whole milliseconds (1–999999999); it only shortens the timeout — a longer or malformed value is ignored |
correlationIdGenerator |
CorrelationId::generate(...) |
Used when the header is missing or invalid |
responseFactory |
null |
When set, ContextExpiredException → expiredStatus, ContextCancelledException → cancelledStatus; otherwise they propagate |
expiredStatus / cancelledStatus |
504 / 499 |
Mapped status codes |
Every request gets a CorrelationId: the incoming header when valid,
otherwise a freshly generated one. The value is also stored in the context
($context->value(CorrelationId::class)), so outbound clients can forward it.
PreflightClient (PSR-18)
use Rasuvaeff\Context\Http\PreflightClient; $client = new PreflightClient( $psr18Client, $context, deadlineHeader: 'X-Request-Timeout-Ms', // optional: send the remaining budget correlationHeader: 'X-Correlation-ID', // optional: forward the correlation id ); $response = $client->sendRequest($request);
Checks assertActive() before starting a request and after receiving the
response. PSR-18 has no transport options, so it cannot impose a timeout on
a blocking client call — configure the client's own timeout, or use
ContextClient with Guzzle. An explicit correlation header on the request
wins over the context's.
ContextClient (Guzzle)
use Rasuvaeff\Context\Http\ContextClient; [$context, $controller] = Context::background()->withTimeout(2.0); $response = (new ContextClient($guzzle, deadlineHeader: 'X-Request-Timeout-Ms'))->send($request, $context); $controller->cancel();
- The remaining budget becomes Guzzle's
timeoutandconnect_timeout. - The request body is sent as its stream; the protocol version is kept.
http_errorsis off: a 4xx/5xx response is returned, as with PSR-18.- A transport failure after the budget ran out is a
ContextExpiredExceptionwhosepreviousis the transport exception; after a cancellation it is aContextCancelledException; otherwise the transport exception propagates. - The constructor rejects an object without a Guzzle-compatible
request().
Propagating one deadline across services
caller ──X-Request-Timeout-Ms: 800──▶ ContextMiddleware(deadlineHeader)
└─ PreflightClient(deadlineHeader) ──X-Request-Timeout-Ms: 790──▶ next service
Use the same header name on both sides. The receiving middleware never
trusts a budget longer than its own timeout.
Queue consumers
Keep the controller with the message owner and derive a fresh context from
the message TTR; call assertActive() before each step and NACK or
dead-letter on ContextExpiredException. The package does not depend on a
queue implementation.
Public API
| Class | Description |
|---|---|
ContextMiddleware |
PSR-15: request context, controller and correlation id attributes; budget header; exception mapping |
PreflightClient |
PSR-18 decorator: pre/post context checks, outbound budget and correlation headers |
ContextClient |
Guzzle-compatible adapter: budget → timeouts, PSR-18 error semantics, expiry mapping |
CorrelationId |
Validated correlation id (isValid(), generate()) |
Security
- Correlation ids are validated: 1–128 characters of
A-Z a-z 0-9 . _ : -. An invalid incoming value is replaced, never logged or forwarded — no log injection, no unbounded header values. - Incoming budgets are untrusted: only whole milliseconds in 1–999999999 are accepted, and they can only shorten the configured timeout.
- Context exceptions mapped to responses carry no details; everything else propagates to your error handler.
- All adapters are cooperative: they prevent work from starting or a response from being consumed after termination, but cannot interrupt a generic blocking PSR-18 call.
Examples
See examples/ for runnable scripts.
| Script | Shows | Needs server? |
|---|---|---|
basic.php |
Middleware reading an upstream budget and correlation id, PreflightClient forwarding both downstream, mapped responses |
no |
Development
No PHP/Composer on the host — run in Docker via the composer:2 image:
docker run --rm -v "$PWD":/app -w /app composer:2 composer install docker run --rm -v "$PWD":/app -w /app composer:2 composer build docker run --rm -v "$PWD":/app -w /app composer:2 composer cs:fix docker run --rm -v "$PWD":/app -w /app composer:2 composer test docker run --rm -v "$PWD":/app -w /app composer:2 composer release-check
Or with Make: make build, make cs-fix, make test, make test-coverage,
make mutation, make release-check.