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:

  1. Your application creates a request
  2. The request passes through each middleware (in the order they were added)
  3. The last middleware passes the request to the HTTP driver
  4. The driver sends the request to the server and receives a response
  5. The response passes back through each middleware (in reverse order)
  6. 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:

  1. Requests pass through middleware in the order they were added to the stack
  2. Responses pass through middleware in reverse order

For example, if you add middleware in this order:

  1. Authentication middleware
  2. Logging middleware
  3. 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\Polyglot\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\Polyglot\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\Polyglot\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\Polyglot\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.