Middleware
Relay supports Guzzle middleware for intercepting and modifying requests and responses. Middleware can add headers, log requests, track timing, and more.
Overview
Section titled “Overview”Middleware wraps request/response processing, allowing you to:
- Modify requests before they’re sent
- Modify responses after they’re received
- Log request/response data
- Track timing and metrics
- Handle errors and retries
Built-in Middleware
Section titled “Built-in Middleware”Header Middleware
Section titled “Header Middleware”use Cline\Relay\Features\Middleware\HeaderMiddleware;use GuzzleHttp\HandlerStack;
class MyConnector extends Connector{ public function middleware(): HandlerStack { $stack = HandlerStack::create();
$stack->push(new HeaderMiddleware([ 'X-Api-Version' => '2024-01', 'X-Client-Name' => 'MyApp', ]));
return $stack; }}Logging Middleware
Section titled “Logging Middleware”use Cline\Relay\Features\Middleware\LoggingMiddleware;use Psr\Log\LoggerInterface;
class MyConnector extends Connector{ public function __construct( private readonly LoggerInterface $logger, ) {}
public function middleware(): HandlerStack { $stack = HandlerStack::create(); $stack->push(new LoggingMiddleware($this->logger));
return $stack; }}Timing Middleware
Section titled “Timing Middleware”use Cline\Relay\Features\Middleware\TimingMiddleware;
$stack->push(new TimingMiddleware(function (float $duration, $request, $response) { metrics()->timing('api.request', $duration);}));Middleware Pipeline
Section titled “Middleware Pipeline”use Cline\Relay\Features\Middleware\MiddlewarePipeline;use Cline\Relay\Features\Middleware\HeaderMiddleware;use Cline\Relay\Features\Middleware\LoggingMiddleware;use Cline\Relay\Features\Middleware\TimingMiddleware;
class MyConnector extends Connector{ public function middleware(): HandlerStack { $pipeline = new MiddlewarePipeline();
$pipeline->push(new HeaderMiddleware([ 'X-Request-ID' => fn () => uniqid(), ])); $pipeline->push(new LoggingMiddleware($this->logger)); $pipeline->push(new TimingMiddleware(function ($duration) { $this->recordTiming($duration); }));
return $pipeline->toHandlerStack(); }}Guzzle Middleware
Section titled “Guzzle Middleware”Retry Middleware
Section titled “Retry Middleware”use GuzzleHttp\HandlerStack;use GuzzleHttp\Middleware;
public function middleware(): HandlerStack{ $stack = HandlerStack::create();
$stack->push(Middleware::retry( decider: function ($retries, $request, $response, $exception) { if ($exception instanceof ConnectException) { return $retries < 3; }
if ($response && $response->getStatusCode() >= 500) { return $retries < 3; }
return false; }, delay: function ($retries) { return $retries * 1000; } ));
return $stack;}History Middleware
Section titled “History Middleware”use GuzzleHttp\Middleware;
$history = [];
$stack->push(Middleware::history($this->history));
public function getHistory(): array{ return $this->history;}Map Request Middleware
Section titled “Map Request Middleware”use GuzzleHttp\Middleware;use Psr\Http\Message\RequestInterface;
$stack->push(Middleware::mapRequest(function (RequestInterface $request) { return $request->withHeader('X-Timestamp', (string) time());}));Map Response Middleware
Section titled “Map Response Middleware”use GuzzleHttp\Middleware;use Psr\Http\Message\ResponseInterface;
$stack->push(Middleware::mapResponse(function (ResponseInterface $response) { return $response->withHeader('X-Processed', 'true');}));Custom Middleware
Section titled “Custom Middleware”use Psr\Http\Message\RequestInterface;use Psr\Http\Message\ResponseInterface;use GuzzleHttp\Promise\PromiseInterface;
class SignatureMiddleware{ public function __construct( private readonly string $secretKey, ) {}
public function __invoke(callable $handler): callable { return function (RequestInterface $request, array $options) use ($handler): PromiseInterface { $signature = $this->sign($request); $request = $request->withHeader('X-Signature', $signature);
return $handler($request, $options)->then( function (ResponseInterface $response) use ($request) { return $response->withHeader( 'X-Request-ID', $request->getHeaderLine('X-Request-ID') ); } ); }; }
private function sign(RequestInterface $request): string { $payload = $request->getMethod() . $request->getUri()->getPath(); return hash_hmac('sha256', $payload, $this->secretKey); }}
$stack->push(new SignatureMiddleware('secret-key'));Middleware Order
Section titled “Middleware Order”$stack = HandlerStack::create();
// 1. First: Add headers$stack->push(new HeaderMiddleware(['X-Api-Key' => $apiKey]));
// 2. Second: Sign request$stack->push(new SignatureMiddleware($secret));
// 3. Third: Log the final request$stack->push(new LoggingMiddleware($logger));
// 4. Fourth: Track timing$stack->push(new TimingMiddleware($callback));
// Execution order:// Request: Headers -> Sign -> Log -> Timing -> [HTTP]// Response: <- Timing <- Log <- Sign <- HeadersUse named middleware for insertion control:
$stack->push(new LoggingMiddleware($logger), 'logging');$stack->push(new TimingMiddleware($callback), 'timing');
$stack->before('logging', new HeaderMiddleware($headers), 'headers');$stack->after('headers', new SignatureMiddleware($secret), 'signature');Common Patterns
Section titled “Common Patterns”Request ID Tracking
Section titled “Request ID Tracking”class RequestIdMiddleware{ public function __invoke(callable $handler): callable { return function (RequestInterface $request, array $options) use ($handler) { $requestId = uniqid('req_', true); $request = $request->withHeader('X-Request-ID', $requestId);
return $handler($request, $options)->then( function (ResponseInterface $response) use ($requestId) { return $response->withHeader('X-Request-ID', $requestId); } ); }; }}Rate Limit Tracking
Section titled “Rate Limit Tracking”class RateLimitMiddleware{ private int $remaining = 0; private int $reset = 0;
public function __invoke(callable $handler): callable { return function (RequestInterface $request, array $options) use ($handler) { return $handler($request, $options)->then( function (ResponseInterface $response) { $this->remaining = (int) $response->getHeaderLine('X-RateLimit-Remaining'); $this->reset = (int) $response->getHeaderLine('X-RateLimit-Reset');
if ($this->remaining < 10) { logger()->warning('Rate limit running low'); }
return $response; } ); }; }}Error Transformation
Section titled “Error Transformation”class ErrorMiddleware{ public function __invoke(callable $handler): callable { return function (RequestInterface $request, array $options) use ($handler) { return $handler($request, $options)->then( function (ResponseInterface $response) use ($request) { if ($response->getStatusCode() >= 400) { $body = json_decode($response->getBody()->getContents(), true);
throw new ApiException( $body['error']['message'] ?? 'Unknown error', $response->getStatusCode() ); }
return $response; } ); }; }}Full Example
Section titled “Full Example”<?php
namespace App\Http\Connectors;
use Cline\Relay\Core\Connector;use Cline\Relay\Features\Middleware\HeaderMiddleware;use Cline\Relay\Features\Middleware\LoggingMiddleware;use Cline\Relay\Features\Middleware\TimingMiddleware;use GuzzleHttp\HandlerStack;use GuzzleHttp\Middleware;use Psr\Log\LoggerInterface;
class ApiConnector extends Connector{ private array $history = [];
public function __construct( private readonly string $apiKey, private readonly LoggerInterface $logger, ) {}
public function baseUrl(): string { return 'https://api.example.com/v1'; }
public function middleware(): HandlerStack { $stack = HandlerStack::create();
$stack->push(new HeaderMiddleware([ 'X-Api-Key' => $this->apiKey, 'X-Client-Version' => '1.0.0', ]), 'headers');
$stack->push(function (callable $handler) { return function ($request, $options) use ($handler) { $request = $request->withHeader('X-Request-ID', uniqid('req_')); return $handler($request, $options); }; }, 'request_id');
$stack->push(new LoggingMiddleware($this->logger), 'logging');
$stack->push(new TimingMiddleware(function ($duration) { $this->logger->debug("Request took {$duration}ms"); }), 'timing');
$stack->push(Middleware::retry( function ($retries, $request, $response, $exception) { return $retries < 3 && ( $exception !== null || ($response && $response->getStatusCode() >= 500) ); }, function ($retries) { return $retries * 500; } ), 'retry');
$stack->push(Middleware::history($this->history), 'history');
return $stack; }
public function getRequestHistory(): array { return $this->history; }}