Issue 1:
Currently page.route(url, handler, options)/browserContext.route(url, handler, options) intercept routes based on url alone. It would be great if we have the ability to intercept based on any request properties like method, header, etc.

Use case

A use case would be the common REST scenario where we want to intercept both GET /items and POST /items.

Very simplified example:

test('items', async ({ page }) => {
// Intercept GET /api/items
await page.route('/api/items', route => route.fulfill({ body: JSON.stringify([{ id: 1, name: 'Item 1' }]) }))

// Intercept POST /api/items
await page.route('/api/items', route => route.fulfill({ body: JSON.stringify({ id: 2, name: 'Item 2' }) }))

await page.goto('/items')
// expect "Item 1" to be in the list

await page.click('role=button[name="New item"]')
// expect "Item 2" to be created in the list
})

The "matcher"(url<string|RegExp|function(URL):boolean>) doesn't allow for any matching on the request itself - only the request url.

The "response"(handler<function(Route, Request)>) doesn't allow us to "skip" to the next interceptor - as the comment says in the source code // There is no chaining, first handler wins..

Existing workarounds

Add single interceptor with conditional handler:

await page.route('/api/items', (route, request) => {
if (request.method() === 'GET') {
return route.fulfill({ body: JSON.stringify([{ id: 1, name: 'Item 1' }]) })
}

if (request.method() === 'POST') {
return route.fulfill({ body: JSON.stringify({ id: 2, name: 'Item 2' })
}

return route.continue()
})

While this is enough in some cases, it's not very atomic and reusable. My idea is to make these a bit more generic and use them in fixtures - making sure that i can easily target just the request i need. Also, the idea of having the matching logic in the handler instead of the matcher just seems wrong.

Use options.times and order the interceptors based on their runtime execution. This seems very fragile and really not ideal.

Proposal

Even though the contribution guide says "Expose as little information as needed. When in doubt, don’t expose new information.", i feel like we should have an escape hatch in the "matcher" function. Two options:

Non-breaking change: adding the request as a second parameter to the url function:

url<string|RegExp|function(URL, request):boolean>

Not the best API but that's the price of non-breaking i guess.

Breaking change: replacing the URL param with the request param in the url function:

url<string|RegExp|function(request):boolean>

This seems like a very robust and flexible way of route matching but it does break BC.

It's a bit unfortunate that we can't reuse the standard glob/regex url matching when we use the function.

This is how i imagine it could look like:

await page.route(
(url, request) => request.method() === 'GET' && minimatch('/api/items', url.toSting()),
route => route.fulfill({ body: JSON.stringify([{ id: 1, name: 'Item 1' }]) })
)

await page.route(
(url, request) => request.method() === 'POST' && minimatch('/api/items', url.toSting()),
route => route.fulfill({ body: JSON.stringify({ id: 2, name: 'Item 2' })
)

I think that having a convenient way to match by method(in addition to url) would be helpful in a lot of situations.

What do you guys think? Do you have other APIs in mind to achieve this or do you think the current workaround methods are good enough?

Issue 2:
I'm seeing weird, intermittent behaviors happen while running tests in the Webkit browser only, and only when it runs inside of the official Docker container... not on my Mac. It appears that some headers (like "Date" and "Expires") get broken into 2 pieces somehow, and then there's the occasional cancelling of requests. Please see the screenshots below.

Something to note: The invalid HTTP headers message in red shows next to a request that looks fine... It's the one above the message that looks janky, as you can see from the headers.

While I suspect a bug, I'm certainly open to the idea that I could be doing something the wrong way if these request/response "interruptions" can be clearly explained. I've definitely not found much online from a simple Google search.

System info

Playwright Version: v1.34.3

Operating Systems:

Locally: MacOS Ventura (M1)

In CI or local Docker: mcr.microsoft.com/playwright:v1.34.3-focal

Browser: Webkit

Other info:

Source code

I provided exact source code that allows reproducing the issue locally.

Link to the GitHub repository with the repro

Prioprietary

or

Config file

Proprietary

Test file (self-contained)

I can't share the test, because it's proprietary, but I've seen the issue from my screenshot in waitForRequest and waitForUrl, when the parameter is a callback function. That may not have anything to do with it, but I don't know.

await test.step(`assert that a service request is made with a token`, async () => {
const tokenedRequest = await page.waitForRequest(
(request) => {
const url = new URL(request.url());
const headers = request.headers();
return url.pathname.includes('/content/') && /Bearer \S{16}/.test(headers?.authorization);
},
{
timeout: 30000,
},
);
expect(tokenedRequest).toBeTruthy();
});

Steps

Proprietary

Expected

I can't give too many details, as it's proprietary, but this is testing Azure/B2C OAuth.

Actual

Sometimes does weird stuff to HTTP headers:

Sometimes cancels requests: