Download the PHP package skedli/http-middleware without Composer
On this page you can find all versions of the php package skedli/http-middleware. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download skedli/http-middleware
More information about skedli/http-middleware
Files in skedli/http-middleware
Package http-middleware
Short Description PSR-15 middleware components for correlation ID propagation, structured logging, error handling, JWT authentication, and idempotency.
License MIT
Homepage https://github.com/skedli/http-middleware
Informations about the package http-middleware
HTTP Middleware
- Overview
- Installation
- How to use
- Correlation ID
- Default usage
- Reusing an existing correlation ID
- Custom provider
- Using the correlation ID in your own logs
- Request and response logging
- Default usage
- What is logged
- Automatic correlation ID binding
- Error handling
- Default usage
- Exception mapping
- Error handling settings
- Logging errors
- Declarative mapping table
- Per-group usage
- Authentication
- Default usage
- Using a JWKS endpoint
- Supported algorithms
- Accessing the authenticated user
- Custom token decoder
- Custom authenticated user
- Builder precedence
- Authorization
- Health check
- Liveness
- Readiness
- Default usage
- Custom health checks
- Doctrine health check
- Non-critical checks
- Multiple instances of the same component
- Degraded readiness
- Drain-aware readiness
- Response format
- Idempotency
- Default usage
- Storing responses with Doctrine DBAL
- Custom scope provider
- Builder options
- Custom store
- Race-condition guarantees
- Conflict response
- Correlation ID
- License
Overview
Provides PSR-15 middleware for HTTP requests, including correlation ID propagation, structured request/response logging, error handling, stateless JWT authentication, extensible health check support, and idempotency enforcement.
Built on top of PSR-15 and PSR-7, the
middleware can be used with any framework that supports the MiddlewareInterface and RequestHandlerInterface
standards.
Installation
How to use
Correlation ID
The middleware reads the Correlation-Id header from the incoming request. If present and non-empty, it reuses the
value. Otherwise, it generates a new UUID v4. In both cases, the correlation ID is:
- Injected as a request attribute (
correlationId) for downstream handlers. - Added to the response as the
Correlation-Idheader.
Default usage
Create the middleware with CorrelationIdMiddleware::create() and register it in your application. The default provider
generates a UUID v4 when no Correlation-Id header is present.
In a Slim 4 application:
The correlation ID is accessible in any handler via the request attribute:
Reusing an existing correlation ID
When the incoming request already contains the Correlation-Id header, the middleware preserves it instead of
generating a new one. This enables end-to-end traceability across service boundaries.
No additional configuration is needed. The middleware handles this automatically.
Custom provider
Implement the CorrelationIdProvider interface to replace the default UUID v4 generation strategy:
Then configure the middleware with the custom provider:
Using the correlation ID in your own logs
The middleware exposes the correlation ID as a request attribute. To include it in your own log entries,
read the attribute and pass it through the PSR-3 $context array. Any PSR-3 logger works. The
tiny-blocks/logger package is one such option, since it implements Psr\Log\LoggerInterface.
Output formatting depends on the logger implementation.
Request and response logging
The LogMiddleware provides structured logging for every HTTP request and response. It captures method, URI,
query parameters, body, status code, and duration automatically.
Default usage
Create the middleware with a tiny-blocks/logger instance:
In a Slim 4 application:
What is logged
The middleware logs two entries per request cycle, one for the incoming request and one for the outgoing response .
Request is always logged at info level:
Response is logged at info for success (2xx/3xx) and error for failures (4xx/5xx):
The query_parameters and body fields are only included when present. A GET request without query parameters
or body produces a minimal log:
Automatic correlation ID binding
When used together with the CorrelationIdMiddleware, the LogMiddleware automatically binds the correlation ID
to the logger context. No additional configuration is needed, just register both middleware in the correct order:
The correlation ID is then automatically included in every log entry:
If the CorrelationIdMiddleware is not registered, the LogMiddleware works normally without the correlation ID.
Error handling
The ErrorMiddleware catches uncaught exceptions during request processing, maps them to structured JSON responses
using a consumer-provided ExceptionMapping, and optionally logs them. When the mapping returns null, the middleware
either returns a generic 500 Internal Server Error response (fallback enabled, the default) or rethrows the exception
(fallback disabled).
When used together with CorrelationIdMiddleware, the ErrorMiddleware automatically binds the correlation ID to the
error log context.
Default usage
withMapping() is required. Provide an ExceptionMapping implementation that converts domain exceptions into
MappedError instances.
Mapped response body:
Fallback response body (unmapped exception, fallback enabled by default):
Exception mapping
ExceptionMapping is the single required dependency. mapTo() receives the thrown exception and returns a
MappedError or null. Returning null defers to the fallback strategy.
MappedError carries four public fields:
| Field | Type | Description |
|---|---|---|
code |
string |
Machine-readable error code included in the response. |
status |
int |
HTTP response status code (400–599). |
message |
string |
Human-readable error description. |
headers |
array<string, string\|string[]> |
Optional HTTP response headers (default []). |
To include response headers, pass headers when constructing a MappedError. The example below
adds Retry-After when login is throttled:
Error handling settings
ErrorHandlingSettings controls response detail exposure and logging verbosity:
| Setting | Default | Description |
|---|---|---|
logErrors |
false |
Enables logging of exceptions when a logger is provided. |
logErrorDetails |
false |
Includes exception class, file, line, and stack trace in the log context. |
displayErrorDetails |
false |
Includes exception class, file, line, and stack trace in the response body. |
Development: full visibility in response and logs:
Fallback response body when displayErrorDetails is true:
Production: log details, minimal response:
Log output:
Logging errors
Provide a tiny-blocks/logger instance and enable logErrors in the
settings. The logger alone is not sufficient. The logErrors setting must be explicitly enabled.
Log output:
If the CorrelationIdMiddleware is not registered, the ErrorMiddleware works normally without the correlation ID.
Declarative mapping table
ExceptionMappingTable is an alternative to writing a custom ExceptionMapping class. It is useful when there are
many exception types to map: rules are registered with a fluent builder instead of imperative if or match chains.
Registration order is preserved and the first matching rule wins.
| Method | Description |
|---|---|
when(class-string<Throwable>) |
Exact-class match. |
whenAny(non-empty-list<class-string<Throwable>>) |
Exact match against any class in the list. |
whenSubclassOf(class-string<Throwable>) |
instanceof match (catches subclasses too). |
mapsTo(code, status, message, headers?) |
Fixed MappedError. |
resolvesWith(Closure) |
Closure receives the matched exception and returns a MappedError. |
Building the table inside mapTo is convenient and stateless per call, but the table is rebuilt on every invocation.
Long-running runtimes can cache the built table as a property or a static variable if profiling shows it matters.
Per-group usage
By default, unmapped exceptions produce a generic 500 fallback response. Set withFallbackOnUnmapped(false) on an
inner middleware to propagate unmapped exceptions outward instead, so a wider scope can handle them.
The outer middleware acts as the catch-all. The inner middleware handles only the exceptions it recognizes and lets the
rest propagate. When withFallbackOnUnmapped(false) is set, the exception is rethrown before any logging occurs.
Authentication
The AuthenticationMiddleware enforces stateless token-based authentication on incoming requests. It extracts the
Bearer token from the Authorization header, validates it using a TokenDecoder, and propagates the authenticated
user context as a request attribute.
No database access is performed validation relies exclusively on the token's cryptographic signature and its claims.
Default usage
Configure the middleware with a signing algorithm and key material. The built-in JwtTokenDecoder handles JWT
validation using firebase/php-jwt.
With RSA (asymmetric):
With HMAC (symmetric):
In a Slim 4 application:
Using a JWKS endpoint
In a microservices architecture, the Identity Provider (IdP) typically exposes its public keys through a JWKS endpoint. The middleware can fetch the public key directly from this endpoint, eliminating the need to manually distribute key material across services.
The JWKS fetch goes through a Skedli\HttpMiddleware\Authentication\JwksProvider supplied by the consumer. The library
does not bundle a concrete HTTP client. Use HttpJwksProvider::with(client:, factory:, jwksUrl:) to combine any PSR-18
client (Guzzle, Symfony HTTP Client, php-http/curl-client, etc.) with a PSR-17 request factory and the JWKS URL.
Timeouts, retries, and TLS configuration are owned by the consumer's client.
If the project already wires a PSR-18 client and a PSR-17 factory for another HTTP integration, the same instances can
be passed to HttpJwksProvider::with(...). The signature is intentionally compatible with
NetworkTransport::with(client:, factory:) from tiny-blocks/http, so a project already using that package can share
the same client and factory arguments. Timeouts, retries, and TLS configuration remain owned by the shared client.
The JWKS fetch is lazy. No network call is made during build(). The public key is resolved on the first incoming
request and cached for the lifetime of the middleware instance. The algorithm defaults to RS256 when not explicitly
set.
In a Slim 4 application with an environment variable:
When the JWKS endpoint is unreachable or returns an invalid response, the middleware returns a 401 Unauthorized
response with a descriptive error message.
When authentication fails, the middleware returns a 401 Unauthorized response:
Possible error messages:
| Message | Cause |
|---|---|
Missing Authorization header. |
The request has no Authorization header. |
Authorization header must use Bearer scheme. |
The header does not start with Bearer. |
Bearer token is empty. |
The header is Bearer with no token value. |
Token is invalid or could not be decoded. |
The token is malformed or the signature does not match. |
Token has expired. |
The token exp claim is in the past. |
Token is missing the subject (sub) claim. |
The token has no sub claim. |
Failed to fetch JWKS from <url>: <reason>. |
The JWKS endpoint is unreachable or timed out. |
Invalid JWKS response from <url>. |
The JWKS JSON has no keys. |
JWKS response does not contain a valid RSA key (...). |
The JWKS key is missing the n or e fields. |
Supported algorithms
The SigningAlgorithm enum defines the supported algorithms:
| Algorithm | Type | Use case |
|---|---|---|
RS256 |
RSA | Public/private key (asymmetric). |
RS384 |
RSA | Public/private key (asymmetric). |
RS512 |
RSA | Public/private key (asymmetric). |
HS256 |
HMAC | Shared secret (symmetric). |
HS384 |
HMAC | Shared secret (symmetric). |
HS512 |
HMAC | Shared secret (symmetric). |
ES256 |
ECDSA | Elliptic curve (asymmetric). |
ES384 |
ECDSA | Elliptic curve (asymmetric). |
Accessing the authenticated user
On successful authentication, the middleware injects an AuthenticatedUser instance as a request attribute. The
AuthenticatedUser is an interface with three methods:
Custom token decoder
Implement the TokenDecoder interface to replace the built-in JWT validation with your own strategy. The decoder
must validate the token locally (stateless), without performing any network call or database query.
Then configure the middleware with the custom decoder:
Custom authenticated user
The AuthenticatedUser is an interface, so you can extend it with additional claims specific to your domain.
Return your custom implementation from your TokenDecoder:
Access the extended claims in your handler:
Builder precedence
When multiple configuration options are provided, the builder resolves them in this order:
| Priority | Configuration | Behavior |
|---|---|---|
| 1st | withTokenDecoder(...) |
Custom decoder wins everything else is ignored. |
| 2nd | withJwksProvider(...) |
Fetches JWKS, converts to PEM, defaults to RS256. |
| 3rd | withKeyMaterial(...) + withAlgorithm(...) |
Uses PEM and algorithm directly. |
Building the middleware without a TokenDecoder, key material, or a JwksProvider throws a TokenValidationFailed
exception. Using key material without an algorithm also throws.
Authorization
RequireAuthorization is a PSR-15 middleware that applies an authorization predicate to the AuthenticatedUser
already verified by AuthenticationMiddleware. It must be placed in the pipeline after authentication: the middleware
reads the user from the AuthenticationMiddleware::AUTHENTICATED_USER_ATTRIBUTE request attribute. When the attribute
is absent, holds a different type, or the predicate returns false, the request is rejected with a 403 response
without invoking the next handler.
Custom TokenDecoder implementations can return a richer AuthenticatedUser subtype, and the predicate parameter
may be typed against that subtype (e.g., fn(TenantAwareUser $user): bool => $user->tenantId() === $tenantId).
When authorization fails, the middleware returns a 403 Forbidden response:
Health check
Two separate endpoints are provided: liveness and readiness. The liveness endpoint is queried by the orchestrator
(e.g., ECS) to decide whether to restart the task. A failure triggers an immediate restart. The readiness endpoint
is queried by the load balancer (e.g., ALB target health check) to decide routing. A failure removes the task from
the pool without restarting it. Both endpoints read the APP_NAME environment variable for the service name,
defaulting to "app" if not set.
Liveness
The LivenessHandler implements RequestHandlerInterface and provides a liveness probe endpoint. It always returns
200 OK without executing any dependency checks, making it suitable for ECS liveness probes where a failure triggers
a task restart.
In a Slim 4 application:
Response:
Readiness
The ReadinessHandler implements RequestHandlerInterface and provides a readiness probe endpoint. It executes
registered dependency checks (e.g., database, cache, message broker) and returns a tri-state overall status derived
from the aggregate outcome. When a drain marker file exists, the endpoint short-circuits immediately without executing
any checks.
Default usage
At least one check must be registered. Calling build() without any check throws ReadinessMisconfigured.
In a Slim 4 application:
Custom health checks
Implement the HealthCheck interface to verify the availability of an external dependency. Each check provides a
component() method that returns a stable generic category (e.g., "cache", "queue") and an optional name()
discriminator label. The check() method returns a HealthCheckResult.
When all checks are UP, the response is 200 OK:
When a critical check is DOWN, the response is 503 Service Unavailable:
If a check throws an unhandled exception, it is caught and reported as DOWN with the exception message.
Doctrine health check
The library provides a built-in DoctrineHealthCheck that verifies database connectivity via
Doctrine DBAL. It receives a Connection instance and
executes a SQL probe query to verify availability.
By default, the check:
- Returns
"database"fromcomponent(). A fixed, stable generic category that never leaks internal schema names. - Returns
nullfromname(). No discriminator label is set unless explicitly configured. - Executes
SELECT 1as the health query. - Is marked as
critical: true.
The optional withName() builder method sets a discriminator label. Use it to distinguish multiple database
connections or, if your monitoring requires it, to explicitly expose the schema name as an opt-in decision:
Non-critical checks
Checks default to critical: true. A non-critical check that is DOWN produces a DEGRADED
overall status and HTTP 200. This is useful for optional dependencies like caches whose unavailability does not stop the
service.
Multiple instances of the same component
When the same component category appears more than once (e.g., a primary and a read replica), use withName() to
add a discriminator label. The component field remains the generic category; name distinguishes the instances.
When both are UP, the response is:
Degraded readiness
When all critical checks are UP but at least one non-critical check is DOWN, the overall status is DEGRADED. The
HTTP status code is still 200 OK. The service is operational and accepts traffic, but one optional dependency is
unavailable.
Drain-aware readiness
Use withDrainMarker(path: ...) to configure a sentinel file path. When the file exists, ReadinessHandler returns
503 Service Unavailable with {"reason":"draining"} immediately, without executing any checks. This short-circuits
dependency calls during graceful shutdown, saving syscalls while the task drains in-flight requests.
When draining, the response is:
Creating the drain marker is the responsibility of the consuming repository, not this library. The container
entrypoint script must trap SIGTERM, create the sentinel file, then forward the signal to PHP-FPM and wait:
Response format
The overall status is tri-state:
Overall status |
Condition | HTTP |
|---|---|---|
OK |
All checks UP | 200 OK |
DEGRADED |
All critical UP, at least one non-critical DOWN | 200 OK |
UNAVAILABLE |
At least one critical DOWN, or drain active | 503 Service Unavailable |
Each entry in the checks array contains:
| Field | Type | Present | Description |
|---|---|---|---|
name |
string | When set | Discriminator label distinguishing instances of the same component. |
status |
string | Always | UP or DOWN. |
message |
string | When DOWN | Present only when the check is DOWN with a reason. |
critical |
boolean | Always | Whether this check affects the overall HTTP status code. |
component |
string | Always | Generic component category (e.g., "database", "cache"). Never leaks internal names. |
duration_in_milliseconds |
float | Always | Time taken to execute the check. |
Idempotency
The IdempotencyMiddleware ensures that mutating HTTP requests (POST, PUT, PATCH, DELETE) are processed
exactly once. When a client sends a request with an Idempotency-Key header, the middleware hashes the request body
(SHA-256) and stores the first successful response. Subsequent requests with the same key replay the stored response,
even if the downstream handler would be invoked again.
If the same key is reused with a different request payload, the middleware returns 409 Conflict without forwarding
the request.
Default usage
Configure the middleware with an IdempotencyReader and IdempotencyWriter implementation. The
DoctrineIdempotencyStore adapter is provided for
Doctrine DBAL:
In a Slim 4 application:
Storing responses with Doctrine DBAL
The DoctrineIdempotencyStore persists idempotency entries in a relational table via
Doctrine DBAL. Build it with the
DoctrineIdempotencyStore::create() builder. The table name defaults to idempotency_keys:
The table must exist before the middleware processes any request. Create it with the following MySQL schema:
Custom scope provider
By default, all requests share a single global namespace. Implement IdempotencyScopeProvider to isolate idempotency
keys per tenant, user, or any other scope:
Then configure the middleware with the custom provider:
Builder options
| Method | Default | Description |
|---|---|---|
withReader(reader: ...) |
required | The IdempotencyReader implementation to use. |
withWriter(writer: ...) |
required | The IdempotencyWriter implementation to use. |
withHeaderName(headerName: ...) |
'Idempotency-Key' |
HTTP header name to read the idempotency key from. |
withTtlSeconds(ttlSeconds: ...) |
86400 (24 hours) |
Time-to-live in seconds for each stored response. |
withMethods(methods: ...) |
['POST','PUT','PATCH','DELETE'] |
HTTP methods subject to idempotency checks. |
withScopeProvider(scopeProvider: ...) |
global scope (empty namespace) | Provider that resolves the namespace for each request. |
Custom store
Implement IdempotencyReader and IdempotencyWriter to use any persistence backend. The save method must be
race-safe:
Race-condition guarantees
DoctrineIdempotencyStore::save() is race-safe. When two concurrent requests arrive with the same key simultaneously,
one
insert wins and the other encounters a unique constraint violation. The losing thread re-reads the row written by the
winner and returns it as though it had been found on the initial lookup.
If the row disappears between the violation and the re-read (expired in a very narrow window), the original entry is returned as a fallback. In all cases the response is consistent: identical payloads produce the same outcome.
Conflict response
When the same idempotency key is reused with a different payload hash, the middleware returns 409 Conflict without
forwarding the request to the handler:
No existing entry is overwritten. The original stored response is preserved.
License
HTTP Middleware is licensed under MIT.
All versions of http-middleware with dependencies
ext-sodium Version *
doctrine/dbal Version ^4.4
firebase/php-jwt Version ^7.0
psr/http-message Version ^2.0
psr/http-server-handler Version ^1.0
psr/http-server-middleware Version ^1.0
ramsey/uuid Version ^4.9
tiny-blocks/environment-variable Version ^1.2
tiny-blocks/http Version ^6.0
tiny-blocks/logger Version ^1.3