diff --git a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts index 82971d624a..b6f8fe80ac 100644 --- a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts @@ -348,8 +348,17 @@ class HarBackend { continue; if (method === 'POST' && postData && candidate.request.postData) { const buffer = await this._loadContent(candidate.request.postData); - if (!buffer.equals(postData)) - continue; + if (!buffer.equals(postData)) { + const boundary = multipartBoundary(headers); + if (!boundary) + continue; + const candidataBoundary = multipartBoundary(candidate.request.headers); + if (!candidataBoundary) + continue; + // Try to match multipart/form-data ignroing boundary as it changes between requests. + if (postData.toString().replaceAll(boundary, '') !== buffer.toString().replaceAll(candidataBoundary, '')) + continue; + } } entries.push(candidate); } @@ -437,3 +446,13 @@ export async function urlToWSEndpoint(progress: Progress|undefined, endpointURL: wsUrl.protocol = wsUrl.protocol === 'https:' ? 'wss:' : 'ws:'; return wsUrl.toString(); } + +function multipartBoundary(headers: HeadersArray) { + const contentType = headers.find(h => h.name.toLowerCase() === 'content-type'); + if (!contentType?.value.includes('multipart/form-data')) + return undefined; + const boundary = contentType.value.match(/boundary=(\S+)/); + if (boundary) + return boundary[1]; + return undefined; +} diff --git a/tests/library/browsercontext-har.spec.ts b/tests/library/browsercontext-har.spec.ts index 6c31346e07..4a7834013a 100644 --- a/tests/library/browsercontext-har.spec.ts +++ b/tests/library/browsercontext-har.spec.ts @@ -412,6 +412,46 @@ it('should update har.zip for context', async ({ contextFactory, server }, testI await expect(page2.locator('body')).toHaveCSS('background-color', 'rgb(255, 192, 203)'); }); +it('should ignore boundary when matching multipart/form-data body', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31495' } +}, async ({ contextFactory, server }, testInfo) => { + server.setRoute('/empty.html', (req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.end(` +
+ + +
`); + }); + server.setRoute('/form.html', (req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.end('
done
'); + }); + + const harPath = testInfo.outputPath('har.zip'); + const context1 = await contextFactory(); + await context1.routeFromHAR(harPath, { update: true }); + const page1 = await context1.newPage(); + await page1.goto(server.PREFIX + '/empty.html'); + const reqPromise = server.waitForRequest('/form.html'); + await page1.locator('button').click(); + await expect(page1.locator('div')).toHaveText('done'); + const req = await reqPromise; + expect((await req.postBody).toString()).toContain('---'); + await context1.close(); + + const context2 = await contextFactory(); + await context2.routeFromHAR(harPath, { notFound: 'abort' }); + const page2 = await context2.newPage(); + await page2.goto(server.PREFIX + '/empty.html'); + const requestPromise = page2.waitForRequest(/.*form.html/); + await page2.locator('button').click(); + const request = await requestPromise; + expect.soft(await request.response()).toBeTruthy(); + expect(request.failure()).toBe(null); + await expect(page2.locator('div')).toHaveText('done'); +}); + it('should update har.zip for page', async ({ contextFactory, server }, testInfo) => { const harPath = testInfo.outputPath('har.zip'); const context1 = await contextFactory(); @@ -428,7 +468,6 @@ it('should update har.zip for page', async ({ contextFactory, server }, testInfo await expect(page2.locator('body')).toHaveCSS('background-color', 'rgb(255, 192, 203)'); }); - it('should update har.zip for page with different options', async ({ contextFactory, server }, testInfo) => { const harPath = testInfo.outputPath('har.zip'); const context1 = await contextFactory();