Skip to main content

nexus-http-toolkit

Production middleware and testing utilities that complement the core HTTP package: structured access logging, W3C trace context propagation, request body size limiting, and an in-process test client.

Install​

terminal
composer require nexus-actors/http-toolkit

Bundled components​

Middleware​

ClassPurpose
AccessLogMiddlewareEmits one PSR-3 log line per request: method, path, status, response size, and latency in ms
BodySizeLimitMiddlewareRejects bodies exceeding a byte limit with 413 Payload Too Large
OriginAllowlistMiddlewareRejects requests whose Origin is not in an exact allow-list — CSWSH / CSRF defense
TraceContextMiddlewareParses/generates W3C traceparent headers; pushes traceId/spanId into MDC

Health checks​

ClassPurpose
HealthCheckInterface — implement name(): string and check(): HealthStatus
HealthCheckRegistryAggregates HealthCheck implementations; iterable at request time
LivenessHandlerOpaque public probe — aggregate up/down status only, no details; safe to expose
HealthCheckHandlerDetailed readiness handler — per-check states; mount on an INTERNAL/authenticated route
HealthStatusValue object: up(array $detail), degraded(array $detail), down(array $detail)
StateEnum: Up, Degraded, Down

Test utilities​

ClassPurpose
HttpTestClientIn-process test client — dispatches requests through a CompiledApplication with no socket
TestResponseFluent assertion wrapper: assertOk(), assertStatus(), assertJsonPath()

Quick example: wiring middleware​

Register AccessLogMiddleware as the outermost layer and BodySizeLimitMiddleware just inside it so oversized bodies are rejected before the body parser runs.

src/Http/Bootstrap.php
use Monadial\Nexus\Http\Toolkit\Middleware\AccessLogMiddleware;
use Monadial\Nexus\Http\Toolkit\Middleware\BodySizeLimitMiddleware;
use Monadial\Nexus\Http\Toolkit\Middleware\TraceContextMiddleware;

$app = HttpApplication::create($system)
->middleware(new AccessLogMiddleware($logger))
->middleware(new TraceContextMiddleware())
->middleware(new BodySizeLimitMiddleware(maxBytes: 10 * 1024 * 1024))
->get('/orders', ListOrdersHandler::class)
->post('/orders', CreateOrderHandler::class);

TraceContextMiddleware sets trace.id, trace.parentSpanId, and trace.spanId as request attributes and writes the corresponding traceparent response header. If nexus-actors/logger is installed the IDs are also pushed into MDC so every log line inside the request carries them automatically.

Health check endpoints​

Split the public liveness probe from the internal readiness detail. LivenessHandler is opaque — it returns only the aggregate up/down state, never check names, details, or exception messages — so it is safe to expose to load balancers and Kubernetes livenessProbe. HealthCheckHandler returns the full per-check breakdown and must be mounted on an internal or authenticated route only, because those details can reveal internal topology and component information.

src/Http/Bootstrap.php
use Monadial\Nexus\Http\Toolkit\Health\HealthCheckHandler;
use Monadial\Nexus\Http\Toolkit\Health\HealthCheckRegistry;
use Monadial\Nexus\Http\Toolkit\Health\LivenessHandler;

$registry = (new HealthCheckRegistry())
->add(new DatabaseHealthCheck($pdo))
->add(new RedisHealthCheck($redis));

$app = HttpApplication::create($system)
->get('/livez', new LivenessHandler($registry)); // public, opaque

$app->get('/readyz', new HealthCheckHandler($registry)) // internal only
->middleware(AuthorizationMiddleware::class);

LivenessHandler returns 200 {"status":"up"} or 503 {"status":"down"} — nothing else.

HealthCheckHandler returns 200 when all checks are Up or Degraded, and 503 when any check is Down. A check that throws is treated as down; the raw exception class and message are redacted by default (they can carry DSNs, hostnames, or credentials) — pass new HealthCheckHandler($registry, includeErrorDetail: true) only on a trusted internal route to surface them. The response body follows an RFC Health JSON-inspired shape:

GET /readyz — example response (internal)
{
"status": "degraded",
"checks": {
"database": { "state": "up", "detail": { "latencyMs": 1.2 } },
"redis": { "state": "degraded", "detail": { "latencyMs": 48.9 } }
}
}

A check that throws is treated as Down with an empty detail by default (the exception is redacted). HealthCheckHandler itself never throws.

In-process testing​

HttpTestClient drives a CompiledApplication without a real socket, making HTTP tests fast and deterministic.

tests/Integration/OrdersApiTest.php
use Monadial\Nexus\Http\Toolkit\Test\HttpTestClient;

$app = HttpApplication::create($system)
->get('/orders/{id}', ShowOrderHandler::class)
->compile();

$client = HttpTestClient::for($app)
->withBearerToken('test-token');

$response = $client->get('/orders/42');

$response->assertOk()->assertJsonPath('id', '42');

Pair HttpTestClient with StepRuntime and call $runtime->drain() between requests for fully deterministic actor-driven tests.

BodySizeLimitMiddleware constructor​

src/Http/Bootstrap.php
// Global 10 MB limit
$app->middleware(new BodySizeLimitMiddleware(maxBytes: 10 * 1024 * 1024));

// Per-route 100 MB limit for upload endpoint
$app->post('/upload', UploadHandler::class)
->middleware(new BodySizeLimitMiddleware(maxBytes: 100 * 1024 * 1024));

The middleware trusts Content-Length for upfront rejection. For streaming/chunked bodies it falls back to getSize() after the body is read. Register it outside any body parser so oversized bodies never reach JSON decoding.

Pass a custom ResponseFactoryInterface and StreamFactoryInterface when your application uses a different PSR-17 implementation:

src/Http/Bootstrap.php
new BodySizeLimitMiddleware(
maxBytes: 5 * 1024 * 1024,
responseFactory: $myResponseFactory,
streamFactory: $myStreamFactory,
);

See also​