Middleware is one of the most powerful features of the Instructor HTTP client API. It allows you to intercept and modify HTTP requests and responses, add functionality to the HTTP client, and create reusable components that can be applied across different applications.
Middleware Concept
Middleware in the Instructor HTTP client API follows the pipeline pattern, where each middleware component gets a chance to process the request before it’s sent and the response after it’s received.
The middleware chain works like this:
- Your application creates a request
- The request passes through each middleware (in the order they were added)
- The last middleware passes the request to the HTTP driver
- The driver sends the request to the server and receives a response
- The response passes back through each middleware (in reverse order)
- Your application receives the final response
This bidirectional flow allows middleware to perform operations both before the request is sent and after the response is received.
The HttpMiddleware Interface
All middleware components must implement the HttpMiddleware
interface:
interface HttpMiddleware
{
public function handle(HttpClientRequest $request, CanHandleHttpRequest $next): HttpClientResponse;
}
The handle
method takes two parameters:
$request
: The HTTP request to process
$next
: The next handler in the middleware chain
The middleware can:
- Modify the request before passing it to the next handler
- Short-circuit the chain by returning a response without calling the next handler
- Process the response from the next handler before returning it
- Wrap the response in a decorator for further processing (especially useful for streaming responses)
The BaseMiddleware Abstract Class
While you can implement the HttpMiddleware
interface directly, the library provides a convenient BaseMiddleware
abstract class that makes it easier to create middleware:
abstract class BaseMiddleware implements HttpMiddleware
{
public function handle(HttpClientRequest $request, CanHandleHttpRequest $next): HttpClientResponse {
// 1) Pre-request logic
$this->beforeRequest($request);
// 2) Get the response from the next handler
$response = $next->handle($request);
// 3) Post-request logic, e.g. logging or rewriting
$response = $this->afterRequest($request, $response);
// 4) Optionally wrap the response if we want to intercept streaming
if ($this->shouldDecorateResponse($request, $response)) {
$response = $this->toResponse($request, $response);
}
// 5) Return the (possibly wrapped) response
return $response;
}
// Override these methods in your subclass
protected function beforeRequest(HttpClientRequest $request): void {}
protected function afterRequest(HttpClientRequest $request, HttpClientResponse $response): HttpClientResponse {
return $response;
}
protected function shouldDecorateResponse(HttpClientRequest $request, HttpClientResponse $response): bool {
return false;
}
protected function toResponse(HttpClientRequest $request, HttpClientResponse $response): HttpClientResponse {
return $response;
}
}
By extending BaseMiddleware
, you only need to override the methods relevant to your middleware’s functionality, making the code more focused and maintainable.
Middleware Stack
The MiddlewareStack
class manages the collection of middleware components. It provides methods to add, remove, and arrange middleware in the stack.
Adding Middleware
There are several ways to add middleware to the stack:
// Create a client
$client = new HttpClient();
// Add a single middleware to the end of the stack
$client->middleware()->append(new LoggingMiddleware());
// Add a single middleware with a name
$client->middleware()->append(new CachingMiddleware(), 'cache');
// Add a single middleware to the beginning of the stack
$client->middleware()->prepend(new AuthenticationMiddleware());
// Add a single middleware to the beginning with a name
$client->middleware()->prepend(new RateLimitingMiddleware(), 'rate-limit');
// Add multiple middleware at once
$client->withMiddleware(
new LoggingMiddleware(),
new RetryMiddleware(),
new TimeoutMiddleware()
);
Named middleware are useful when you need to reference them later, for example, to remove or replace them.
Removing Middleware
You can remove middleware from the stack by name:
// Remove a middleware by name
$client->middleware()->remove('cache');
Replacing Middleware
You can replace a middleware with another one:
// Replace a middleware with a new one
$client->middleware()->replace('cache', new ImprovedCachingMiddleware());
Clearing Middleware
You can remove all middleware from the stack:
// Clear all middleware
$client->middleware()->clear();
Checking Middleware
You can check if a middleware exists in the stack:
// Check if a middleware exists
if ($client->middleware()->has('rate-limit')) {
// The 'rate-limit' middleware exists
}
Getting Middleware
You can get a middleware from the stack by name or index:
// Get a middleware by name
$rateLimitMiddleware = $client->middleware()->get('rate-limit');
// Get a middleware by index
$firstMiddleware = $client->middleware()->get(0);
Middleware Order
The order of middleware in the stack is important because:
- Requests pass through middleware in the order they were added to the stack
- Responses pass through middleware in reverse order
For example, if you add middleware in this order:
- Authentication middleware
- Logging middleware
- Retry middleware
The execution flow will be:
- Request: Authentication → Logging → Retry → HTTP Driver
- Response: Retry → Logging → Authentication → Your Application
This allows you to nest functionality appropriately. For instance, the authentication middleware might add headers to the request and then verify the authentication status of the response before your application receives it.
Middleware Application Example
Here’s an example of how middleware is applied in a request-response cycle:
// Create a client with middleware
$client = new HttpClient();
$client->withMiddleware(
new LoggingMiddleware(), // 1. Log the request and response
new RetryMiddleware(), // 2. Retry failed requests
new TimeoutMiddleware() // 3. Custom timeout handling
);
// Create a request
$request = new HttpClientRequest(
url: 'https://api.example.com/data',
method: 'GET',
headers: ['Accept' => 'application/json'],
body: [],
options: []
);
// Handle the request (middleware execution flow):
// 1. LoggingMiddleware processes the request (logs outgoing request)
// 2. RetryMiddleware processes the request
// 3. TimeoutMiddleware processes the request
// 4. HTTP driver sends the request
// 5. TimeoutMiddleware processes the response
// 6. RetryMiddleware processes the response (may retry on certain status codes)
// 7. LoggingMiddleware processes the response (logs incoming response)
$response = $client->handle($request);
Built-in Middleware
The Instructor HTTP client API includes several built-in middleware components for common tasks:
Debug Middleware
The DebugMiddleware
logs detailed information about HTTP requests and responses:
use Cognesy\Http\Middleware\Debug\DebugMiddleware;
// Enable debug middleware
$client->withMiddleware(new DebugMiddleware());
// Or use the convenience method
$client->withDebug(true);
The debug middleware logs:
- Request URLs
- Request headers
- Request bodies
- Response headers
- Response bodies
- Streaming response data
You can configure which aspects to log in the config/debug.php
file:
return [
'http' => [
'enabled' => true, // Enable/disable debug
'trace' => false, // Dump HTTP trace information
'requestUrl' => true, // Dump request URL to console
'requestHeaders' => true, // Dump request headers to console
'requestBody' => true, // Dump request body to console
'responseHeaders' => true, // Dump response headers to console
'responseBody' => true, // Dump response body to console
'responseStream' => true, // Dump stream data to console
'responseStreamByLine' => true, // Dump stream as full lines or raw chunks
],
];
BufferResponse Middleware
The BufferResponseMiddleware
stores response bodies and streaming chunks for reuse:
use Cognesy\Http\Middleware\BufferResponse\BufferResponseMiddleware;
// Add buffer response middleware
$client->withMiddleware(new BufferResponseMiddleware());
This middleware is useful when you need to access a response body or stream multiple times, as it stores the data after the first access.
StreamByLine Middleware
The StreamByLineMiddleware
processes streaming responses line by line:
use Cognesy\Http\Middleware\StreamByLine\StreamByLineMiddleware;
// Add stream by line middleware
$client->withMiddleware(new StreamByLineMiddleware());
You can customize how lines are processed by providing a parser function:
$lineParser = function (string $line) {
$trimmedLine = trim($line);
if (empty($trimmedLine)) {
return null; // Skip empty lines
}
return json_decode($trimmedLine, true);
};
$client->withMiddleware(new StreamByLineMiddleware($lineParser));
RecordReplay Middleware
The RecordReplayMiddleware
records HTTP interactions and can replay them later:
use Cognesy\Http\Middleware\RecordReplay\RecordReplayMiddleware;
// Create a record/replay middleware in record mode
$recordReplayMiddleware = new RecordReplayMiddleware(
mode: RecordReplayMiddleware::MODE_RECORD,
storageDir: __DIR__ . '/recordings',
fallbackToRealRequests: true
);
// Add it to the client
$client->withMiddleware($recordReplayMiddleware);
The middleware has three modes:
MODE_PASS
: Normal operation, no recording or replaying
MODE_RECORD
: Records all HTTP interactions to the storage directory
MODE_REPLAY
: Replays recorded interactions instead of making real requests
This is particularly useful for:
- Testing: Record real API responses once, then replay them in tests
- Offline development: Develop without access to real APIs
- Demo environments: Ensure consistent responses for demos
- Performance testing: Replay recorded responses to eliminate API variability
Example of switching modes:
// Switch to replay mode
$recordReplayMiddleware->setMode(RecordReplayMiddleware::MODE_REPLAY);
// Switch to record mode
$recordReplayMiddleware->setMode(RecordReplayMiddleware::MODE_RECORD);
// Switch to pass-through mode
$recordReplayMiddleware->setMode(RecordReplayMiddleware::MODE_PASS);
Example Middleware Combinations
Here are some common middleware combinations for different scenarios:
Debugging Setup
$client = new HttpClient();
$client->withMiddleware(
new BufferResponseMiddleware(), // Buffer responses for reuse
new DebugMiddleware() // Log requests and responses
);
API Client Setup
$client = new HttpClient();
$client->withMiddleware(
new RetryMiddleware(maxRetries: 3, retryDelay: 1), // Retry failed requests
new AuthenticationMiddleware($apiKey), // Handle authentication
new RateLimitingMiddleware(maxRequests: 100), // Respect rate limits
new LoggingMiddleware() // Log API interactions
);
Testing Setup
$client = new HttpClient();
$client->withMiddleware(
new RecordReplayMiddleware(RecordReplayMiddleware::MODE_REPLAY) // Replay recorded responses
);
Streaming Setup
$client = new HttpClient();
$client->withMiddleware(
new StreamByLineMiddleware(), // Process streaming responses line by line
new BufferResponseMiddleware() // Buffer responses for reuse
);
By combining middleware components, you can create a highly customized HTTP client that handles complex requirements while keeping your application code clean and focused.
In the next chapter, we’ll explore how to create custom middleware components to handle specific requirements.