Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 🎭 Playwright

[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-144.0.7559.20-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-145.0.2-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-26.0-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-informational)](https://aka.ms/playwright/discord)
[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-144.0.7559.20-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-146.0.1-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-26.0-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-informational)](https://aka.ms/playwright/discord)

## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)

Expand All @@ -10,7 +10,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->144.0.7559.20<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->26.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->145.0.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->146.0.1<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |

Headless execution is supported for all browsers on all platforms. Check out [system requirements](https://playwright.dev/docs/intro#system-requirements) for details.

Expand Down
50 changes: 50 additions & 0 deletions browser_patches/firefox/juggler/NetworkObserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,56 @@ function overrideRequestHeaders(httpChannel, headers) {
appendExtraHTTPHeaders(httpChannel, headers);
}

// Forbidden request headers according to https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_request_header
// These headers cannot be set or modified programmatically.
const FORBIDDEN_HEADER_NAMES = new Set([
'accept-charset',
'accept-encoding',
'access-control-request-headers',
'access-control-request-method',
'connection',
'content-length',
'cookie',
'date',
'dnt',
'expect',
'host',
'keep-alive',
'origin',
'referer',
'set-cookie',
'te',
'trailer',
'transfer-encoding',
'upgrade',
'via',
]);

// Forbidden method names for X-HTTP-Method-* headers
const FORBIDDEN_METHODS = new Set(['CONNECT', 'TRACE', 'TRACK']);

function isForbiddenHeader(name, value) {
const lowerName = name.toLowerCase();

if (FORBIDDEN_HEADER_NAMES.has(lowerName))
return true;

if (lowerName.startsWith('proxy-'))
return true;

if (lowerName.startsWith('sec-'))
return true;

if (lowerName === 'x-http-method' ||
lowerName === 'x-http-method-override' ||
lowerName === 'x-method-override') {
if (value && FORBIDDEN_METHODS.has(value.toUpperCase()))
return true;
}

return false;
}

const redirectStatus = [301, 302, 303, 307, 308];

function filterHeadersForRedirect(headers, requestMethod, status) {
Expand Down
5 changes: 4 additions & 1 deletion docs/src/api/class-route.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,10 @@ The [`option: headers`] option applies to both the routed request and any redire
[`method: Route.continue`] will immediately send the request to the network, other matching handlers won't be invoked. Use [`method: Route.fallback`] If you want next matching handler in the chain to be invoked.

:::warning
The `Cookie` header cannot be overridden using this method. If a value is provided, it will be ignored, and the cookie will be loaded from the browser's cookie store. To set custom cookies, use [`method: BrowserContext.addCookies`].
Some request headers are **forbidden** and cannot be overridden (for example, `Cookie`, `Host`, `Content-Length` and others, see [this MDN page](https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_request_header) for full list).
If an override is provided for a forbidden header, it will be ignored and the original request header will be used.

To set custom cookies, use [`method: BrowserContext.addCookies`].
:::

### option: Route.continue.url
Expand Down
8 changes: 6 additions & 2 deletions packages/playwright-client/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21121,8 +21121,12 @@ export interface Route {
* [route.fallback([options])](https://playwright.dev/docs/api/class-route#route-fallback) If you want next matching
* handler in the chain to be invoked.
*
* **NOTE** The `Cookie` header cannot be overridden using this method. If a value is provided, it will be ignored,
* and the cookie will be loaded from the browser's cookie store. To set custom cookies, use
* **NOTE** Some request headers are **forbidden** and cannot be overridden (for example, `Cookie`, `Host`,
* `Content-Length` and others, see
* [this MDN page](https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_request_header) for full list). If an
* override is provided for a forbidden header, it will be ignored and the original request header will be used.
*
* To set custom cookies, use
* [browserContext.addCookies(cookies)](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-cookies).
*
* @param options
Expand Down
6 changes: 3 additions & 3 deletions packages/playwright-core/browsers.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@
},
{
"name": "firefox",
"revision": "1507",
"revision": "1509",
"installByDefault": true,
"browserVersion": "145.0.2",
"browserVersion": "146.0.1",
"title": "Firefox"
},
{
"name": "firefox-beta",
"revision": "1502",
"revision": "1504",
"installByDefault": false,
"browserVersion": "146.0b8",
"title": "Firefox Beta"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,10 @@ export class CRNetworkManager {
if (redirectedFrom || (!this._userRequestInterceptionEnabled && this._protocolRequestInterceptionEnabled)) {
// Chromium does not preserve header overrides between redirects, so we have to do it ourselves.
headersOverride = redirectedFrom?._originalRequestRoute?._alreadyContinuedParams?.headers;
if (headersOverride) {
const originalHeaders = Object.entries(requestPausedEvent.request.headers).map(([name, value]) => ({ name, value }));
headersOverride = network.applyHeadersOverrides(originalHeaders, headersOverride);
}
requestPausedSessionInfo!.session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId, headers: headersOverride });
} else {
route = new RouteImpl(requestPausedSessionInfo!.session, requestPausedEvent.requestId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1702,7 +1702,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Firefox HiDPI": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:145.0.2) Gecko/20100101 Firefox/145.0.2",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0.1) Gecko/20100101 Firefox/146.0.1",
"screen": {
"width": 1792,
"height": 1120
Expand Down Expand Up @@ -1762,7 +1762,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Firefox": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:145.0.2) Gecko/20100101 Firefox/145.0.2",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0.1) Gecko/20100101 Firefox/146.0.1",
"screen": {
"width": 1920,
"height": 1080
Expand Down
63 changes: 59 additions & 4 deletions packages/playwright-core/src/server/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,62 @@ export function isLocalHostname(hostname: string): boolean {
return hostname === 'localhost' || hostname.endsWith('.localhost');
}

// Forbidden request headers according to https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_request_header
// These headers cannot be set or modified programmatically.
const FORBIDDEN_HEADER_NAMES = new Set([
'accept-charset',
'accept-encoding',
'access-control-request-headers',
'access-control-request-method',
'connection',
'content-length',
'cookie',
'date',
'dnt',
'expect',
'host',
'keep-alive',
'origin',
'referer',
'set-cookie',
'te',
'trailer',
'transfer-encoding',
'upgrade',
'via',
]);

// Forbidden method names for X-HTTP-Method-* headers
const FORBIDDEN_METHODS = new Set(['CONNECT', 'TRACE', 'TRACK']);

function isForbiddenHeader(name: string, value?: string): boolean {
const lowerName = name.toLowerCase();

if (FORBIDDEN_HEADER_NAMES.has(lowerName))
return true;

if (lowerName.startsWith('proxy-'))
return true;

if (lowerName.startsWith('sec-'))
return true;

if (lowerName === 'x-http-method' ||
lowerName === 'x-http-method-override' ||
lowerName === 'x-method-override') {
if (value && FORBIDDEN_METHODS.has(value.toUpperCase()))
return true;
}

return false;
}

export function applyHeadersOverrides(original: HeadersArray, overrides: HeadersArray): HeadersArray {
const forbiddenHeaders = original.filter(header => isForbiddenHeader(header.name, header.value));
const allowedHeaders = overrides.filter(header => !isForbiddenHeader(header.name, header.value));
return mergeHeaders([allowedHeaders, forbiddenHeaders]);
}

// Rollover to 5-digit year:
// 253402300799 == Fri, 31 Dec 9999 23:59:59 +0000 (UTC)
// 253402300800 == Sat, 1 Jan 1000 00:00:00 +0000 (UTC)
Expand Down Expand Up @@ -369,10 +425,9 @@ export class Route extends SdkObject {
throw new Error('New URL must have same protocol as overridden URL');
}
if (overrides.headers) {
overrides.headers = overrides.headers?.filter(header => {
const headerName = header.name.toLowerCase();
return headerName !== 'cookie' && headerName !== 'host';
});
// Filter out forbidden headers from overrides - they cannot be overridden
// and will be passed as-is from the original request
overrides.headers = applyHeadersOverrides(this._request._headers, overrides.headers);
}
overrides = this._request._applyOverrides(overrides);

Expand Down
8 changes: 6 additions & 2 deletions packages/playwright-core/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21121,8 +21121,12 @@ export interface Route {
* [route.fallback([options])](https://playwright.dev/docs/api/class-route#route-fallback) If you want next matching
* handler in the chain to be invoked.
*
* **NOTE** The `Cookie` header cannot be overridden using this method. If a value is provided, it will be ignored,
* and the cookie will be loaded from the browser's cookie store. To set custom cookies, use
* **NOTE** Some request headers are **forbidden** and cannot be overridden (for example, `Cookie`, `Host`,
* `Content-Length` and others, see
* [this MDN page](https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_request_header) for full list). If an
* override is provided for a forbidden header, it will be ignored and the original request header will be used.
*
* To set custom cookies, use
* [browserContext.addCookies(cookies)](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-cookies).
*
* @param options
Expand Down
33 changes: 12 additions & 21 deletions tests/page/page-request-continue.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,24 +48,16 @@ it('should not allow to override unsafe HTTP headers', async ({ page, server, br
const serverRequestPromise = server.waitForRequest('/empty.html');
page.goto(server.EMPTY_PAGE).catch(() => {});
const route = await routePromise;
const error = await route.continue({
await route.continue({
headers: {
...route.request().headers(),
host: 'bar',
trailer: 'baz',
}
}).catch(e => e);
if (browserName === 'chromium' || isElectron) {
expect(error.message).toContain('Unsafe header');
serverRequestPromise.catch(() => {});
} else {
expect(error).toBeFalsy();
// These lines just document current behavior in FF and WK,
// we don't necessarily want to maintain this behavior.
const serverRequest = await serverRequestPromise;
expect(serverRequest.headers['trailer']).toBe('baz');
expect(serverRequest.headers['host']).toBe(new URL(server.EMPTY_PAGE).host);
}
});
const serverRequest = await serverRequestPromise;
expect(serverRequest.headers['trailer']).toBe(undefined);
expect(serverRequest.headers['host']).toBe(new URL(server.EMPTY_PAGE).host);
});

it('should delete header with undefined value', async ({ page, server, browserName }) => {
Expand Down Expand Up @@ -349,20 +341,19 @@ it('should work with Cross-Origin-Opener-Policy', async ({ page, server, browser
expect(response.request().failure()).toBeNull();
});

it('should delete the origin header', async ({ page, server, isAndroid, browserName }) => {
it('should not delete the origin header', async ({ page, server, isAndroid }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/13106' });
it.skip(isAndroid, 'No cross-process on Android');
it.fail(browserName === 'webkit', 'Does not delete origin in webkit');

await page.goto(server.PREFIX + '/empty.html');
server.setRoute('/something', (request, response) => {
response.writeHead(200, { 'Access-Control-Allow-Origin': '*' });
response.end('done');
});
let interceptedRequest;
let interceptedOrigin;
await page.route(server.CROSS_PROCESS_PREFIX + '/something', async (route, request) => {
interceptedRequest = request;
const headers = await request.allHeaders();
interceptedOrigin = headers.origin;
delete headers['origin'];
void route.continue({ headers });
});
Expand All @@ -375,11 +366,11 @@ it('should delete the origin header', async ({ page, server, isAndroid, browserN
server.waitForRequest('/something')
]);
expect(text).toBe('done');
expect(interceptedRequest.headers()['origin']).toEqual(undefined);
expect(serverRequest.headers.origin).toBeFalsy();
expect(interceptedOrigin).toEqual(server.PREFIX);
expect(serverRequest.headers.origin).toBe(server.PREFIX);
});

it('should continue preload link requests', async ({ page, server, browserName }) => {
it('should continue preload link requests', async ({ page, server }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/16745' });
let intercepted = false;
await page.route('**/one-style.css', route => {
Expand All @@ -404,7 +395,7 @@ it('should continue preload link requests', async ({ page, server, browserName }

it('should respect set-cookie in redirect response', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/35154' }
}, async ({ page, server, browserName }) => {
}, async ({ page, server }) => {
await page.goto(server.EMPTY_PAGE);
await page.setContent('<a href="/set-cookie-redirect">Set cookie</a>');
server.setRoute('/set-cookie-redirect', (request, response) => {
Expand Down
Loading