From a93ad3dadea86e3e1d555c5bb9c2a19458db656b Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 21 May 2024 09:15:33 +0200 Subject: [PATCH] fix(fetch): allow UTF-8 in Location header (#30904) --- packages/playwright-core/src/server/fetch.ts | 9 ++++-- tests/library/browsercontext-fetch.spec.ts | 29 ++++++++++---------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index 56125f2e5d..e15c5aca8a 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -336,12 +336,15 @@ export abstract class APIRequestContext extends SdkObject { redirectOptions.rejectUnauthorized = false; // HTTP-redirect fetch step 4: If locationURL is null, then return response. - if (response.headers.location) { + // Best-effort UTF-8 decoding, per spec it's US-ASCII only, but browsers are more lenient. + // Node.js parses it as Latin1 via std::v8::String, so we convert it to UTF-8. + const locationHeaderValue = Buffer.from(response.headers.location ?? '', 'latin1').toString('utf8'); + if (locationHeaderValue) { let locationURL; try { - locationURL = new URL(response.headers.location, url); + locationURL = new URL(locationHeaderValue, url); } catch (error) { - reject(new Error(`uri requested responds with an invalid redirect URL: ${response.headers.location}`)); + reject(new Error(`uri requested responds with an invalid redirect URL: ${locationHeaderValue}`)); request.destroy(); return; } diff --git a/tests/library/browsercontext-fetch.spec.ts b/tests/library/browsercontext-fetch.spec.ts index 0efb4bacb6..e4d7a6dbf3 100644 --- a/tests/library/browsercontext-fetch.spec.ts +++ b/tests/library/browsercontext-fetch.spec.ts @@ -198,6 +198,20 @@ it('should follow redirects', async ({ context, server }) => { expect(await response.json()).toEqual({ foo: 'bar' }); }); +it('should follow redirects correctly when Location header contains UTF-8 characters', async ({ context, server }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30903' }); + server.setRoute('/redirect', (req, res) => { + // Node.js only allows US-ASCII, so we can't send invalid headers directly. Sending it as a raw response instead. + res.socket.write('HTTP/1.1 301 Moved Permanently\r\n'); + res.socket.write(`Location: ${server.PREFIX}/empty.html?message=マスクПривет\r\n`); + res.socket.write('\r\n'); + res.socket.uncork(); + res.socket.end(); + }); + const response = await context.request.get(server.PREFIX + '/redirect'); + expect(response.url()).toBe(server.PREFIX + '/empty.html?' + new URLSearchParams({ message: 'マスクПривет' })); +}); + it('should add cookies from Set-Cookie header', async ({ context, page, server }) => { server.setRoute('/setcookie.html', (req, res) => { res.setHeader('Set-Cookie', ['session=value', 'foo=bar; max-age=3600']); @@ -794,21 +808,6 @@ it('should respect timeout after redirects', async function({ context, server }) expect(error.message).toContain(`Request timed out after 100ms`); }); -it('should throw on a redirect with an invalid URL', async ({ context, server }) => { - server.setRedirect('/redirect', '/test'); - server.setRoute('/test', (req, res) => { - // Node.js prevents us from responding with an invalid header, therefore we manually write the response. - const conn = res.connection!; - conn.write('HTTP/1.1 302\r\n'); - conn.write('Location: https://здравствуйте/\r\n'); - conn.write('\r\n'); - conn.uncork(); - conn.end(); - }); - const error = await context.request.get(server.PREFIX + '/redirect').catch(e => e); - expect(error.message).toContain('apiRequestContext.get: uri requested responds with an invalid redirect URL'); -}); - it('should not hang on a brotli encoded Range request', async ({ context, server }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/18190' }); it.skip(+process.versions.node.split('.')[0] < 18);