-
Notifications
You must be signed in to change notification settings - Fork 2
Middleware
Pair API middleware is based on the Pair\Api\Middleware interface plus Pair\Api\MiddlewarePipeline.
Middleware is the reusable request-guard layer of the Pair API stack.
Use it for cross-cutting concerns such as:
- CORS
- throttling
- bearer-token checks
- tenant or workspace headers
- feature flags or maintenance windows
Every middleware receives:
Request $requestcallable $next
and can either:
- continue the pipeline with
$next($request) - stop the request immediately by returning an API response
interface Middleware {
public function handle(Request $request, callable $next): mixed;
}This single method is the main method of every middleware class.
MiddlewarePipeline runs middleware in FIFO order:
- first added = first executed
- last added = closest to the final action
ApiController exposes this through:
$this->middleware(...)$this->runMiddleware(function () { ... })
Example:
protected function _init(): void
{
parent::_init();
// The default throttle is already mounted by ApiController.
$this->middleware(new \Pair\Api\CorsMiddleware());
$this->middleware(new RequireBearerMiddleware());
}
public function ordersAction(): void
{
$this->runMiddleware(function () {
// Runs only after all middleware pass.
\Pair\Api\ApiResponse::respond(['ok' => true]);
});
}Important: ApiController currently mounts the default throttle inside parent::_init(), so middleware added later in _init() runs after that throttle.
If you need a different order, override registerDefaultMiddleware().
protected function registerDefaultMiddleware(): void
{
// Puts CORS before the throttle for this controller family.
$this->middleware(new \Pair\Api\CorsMiddleware());
$this->middleware(new \Pair\Api\ThrottleMiddleware(60, 60));
}Registers one middleware in the controller pipeline.
Executes the middleware stack and returns the first explicit value produced by the middleware chain or final action callback.
These are the two API-controller methods you will use most often when working with middleware.
Typical responsibilities:
- emits
Access-Control-*headers - handles preflight
OPTIONSrequests - can short-circuit by returning an explicit HTTP
204response
Uses RateLimiter to restrict request frequency by the best available identity:
sid- bearer token
- authenticated user
- client IP
Example:
// Allows up to 120 requests every 60 seconds.
new \Pair\Api\ThrottleMiddleware(120, 60);When the limit is exceeded, Pair returns TOO_MANY_REQUESTS with HTTP 429, Retry-After, and X-RateLimit-* headers.
use Pair\Api\Middleware;
use Pair\Api\Request;
use Pair\Api\ApiResponse;
class RequireJsonMiddleware implements Middleware {
public function handle(Request $request, callable $next): mixed
{
// Returns an explicit error response if the payload is not JSON.
if (!$request->isJson()) {
return ApiResponse::errorResponse('UNSUPPORTED_MEDIA_TYPE');
}
// Continues with the next middleware or final action.
return $next($request);
}
}use Pair\Api\Middleware;
use Pair\Api\Request;
use Pair\Api\ApiResponse;
class RequireBearerMiddleware implements Middleware {
public function handle(Request $request, callable $next): mixed
{
// Requires the Authorization: Bearer header.
if (!$request->bearerToken()) {
return ApiResponse::errorResponse('AUTH_TOKEN_MISSING');
}
// Continues only when the token is present.
return $next($request);
}
}class RequireTenantMiddleware implements \Pair\Api\Middleware {
public function handle(\Pair\Api\Request $request, callable $next): mixed
{
// Reads the custom tenant header.
$tenant = $request->header('X-Tenant-Id');
if (!$tenant) {
// Returns the normalized API error without sending output directly.
return \Pair\Api\ApiResponse::errorResponse('BAD_REQUEST', [
'detail' => 'Missing X-Tenant-Id header',
]);
}
// Continues the pipeline when the header is present.
return $next($request);
}
}protected function _init(): void
{
parent::_init();
// Adds CORS after the default throttle.
$this->middleware(new \Pair\Api\CorsMiddleware());
// Requires a bearer token for every action in this controller.
$this->middleware(new RequireBearerMiddleware());
}
public function meAction(): void
{
$this->runMiddleware(function () {
// The destination runs only after all middleware pass.
\Pair\Api\ApiResponse::respond(['ok' => true]);
});
}Middleware itself has only one public contract method, but the surrounding pipeline is equally important:
-
MiddlewarePipeline::add(Middleware $middleware): staticAppends one middleware to the stack. -
MiddlewarePipeline::run(Request $request, callable $destination): voidExecutes the full FIFO pipeline.
- Calling
$next()more than once inside the same middleware. - Forgetting to call
$next($request)when the middleware should allow the request through. - Adding a second throttle middleware without accounting for the default one already added by
ApiController. - Assuming CORS runs before the default throttle when you only append middleware after
parent::_init(). - Putting CORS after auth/throttle when preflight requests should short-circuit first.
See also: MiddlewarePipeline, RateLimiter, ThrottleMiddleware, Request, API.