If you never heard about playwright, this will be a very technical post, so you should probably skip it.
Playwright HAR storing
Playwright allows you to store and replay HTTP requests from your app via something like this:
await page.routeFromHAR('./test/e2e/har/openai-requests.har', {
url: 'https://api.openai.com/v1/**',
update: true,
});
It’s pretty sweet – you mock API responses on the network layer to later reuse them in tests.
Problem: Polling requests
Turns out when you are using polling requests (like in the OpenAI retrieve run endpoint), you get the first cached response in the loop.
This is designed behaviour. From Playwright docs:
HAR replay matches URL and HTTP method strictly. For POST requests, it also matches POST payloads strictly. If multiple recordings match a request, the one with the most matching headers is picked. An entry resulting in a redirect will be followed automatically.
Because the OpenAI retrieve run endpoint has the same URL through all polling calls, the Playwright network does not know how to differentiate between them.
Solution: custom header
But wait a minute! If you look closely at the docs:
If multiple recordings match a request, the one with the most matching headers is picked.
Can we introduce “most matching header” ourselves? In the most naive implementation, we would just iterate a counter on each request and add it as a header.
Here its overriding the default OpenAI fetch implementation to provide an extra header:
const newOpenai = new OpenAI( {
apiKey: accessKey,
dangerouslyAllowBrowser: true,
fetch: async (url, init ) => {
// We are overriding fetch to add request counter header so that we can differentiate between polling requests in tests.
if ( ! window.reqCounter ) {
window.reqCounter = 0;
}
window.reqCounter++;
init.headers['x-req-counter'] = window.reqCounter;
return fetch(url, init);
},
} );
const newThread = await newOpenai.beta.threads.create();
Now, playwright sees these as distinct requests and retrieves appropriate ones from the HAR file.
