Skip to content

Middleware

Relay supports Guzzle middleware for intercepting and modifying requests and responses. Middleware can add headers, log requests, track timing, and more.

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
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;
}
}
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;
}
}
use Cline\Relay\Features\Middleware\TimingMiddleware;
$stack->push(new TimingMiddleware(function (float $duration, $request, $response) {
metrics()->timing('api.request', $duration);
}));
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();
}
}
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;
}
use GuzzleHttp\Middleware;
$history = [];
$stack->push(Middleware::history($this->history));
public function getHistory(): array
{
return $this->history;
}
use GuzzleHttp\Middleware;
use Psr\Http\Message\RequestInterface;
$stack->push(Middleware::mapRequest(function (RequestInterface $request) {
return $request->withHeader('X-Timestamp', (string) time());
}));
use GuzzleHttp\Middleware;
use Psr\Http\Message\ResponseInterface;
$stack->push(Middleware::mapResponse(function (ResponseInterface $response) {
return $response->withHeader('X-Processed', 'true');
}));
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'));
$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 <- Headers

Use 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');
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);
}
);
};
}
}
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;
}
);
};
}
}
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;
}
);
};
}
}
<?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;
}
}