Skip to content

Commit 672f3f9

Browse files
authored
feat(popups): introduce BrowserContext.setDefaultHTTPHeaders (#1116)
1 parent 4f69930 commit 672f3f9

14 files changed

+134
-30
lines changed

docs/api.md

+12
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ Indicates that the browser is connected.
199199
- `accuracy` <[number]> Optional non-negative accuracy value.
200200
- `locale` <?[string]> Specify user locale, for example `en-GB`, `de-DE`, etc. Locale will affect `navigator.language` value, `Accept-Language` request header value as well as number and date formatting rules.
201201
- `permissions` <[Object]> A map from origin keys to permissions values. See [browserContext.setPermissions](#browsercontextsetpermissionsorigin-permissions) for more details.
202+
- `extraHTTPHeaders` <[Object]> An object containing additional HTTP headers to be sent with every request. All header values must be strings.
202203
- returns: <[Promise]<[BrowserContext]>>
203204

204205
Creates a new browser context. It won't share cookies/cache with other browser contexts.
@@ -232,6 +233,7 @@ Creates a new browser context. It won't share cookies/cache with other browser c
232233
- `accuracy` <[number]> Optional non-negative accuracy value.
233234
- `locale` <?[string]> Specify user locale, for example `en-GB`, `de-DE`, etc. Locale will affect `navigator.language` value, `Accept-Language` request header value as well as number and date formatting rules.
234235
- `permissions` <[Object]> A map from origin keys to permissions values. See [browserContext.setPermissions](#browsercontextsetpermissionsorigin-permissions) for more details.
236+
- `extraHTTPHeaders` <[Object]> An object containing additional HTTP headers to be sent with every request. All header values must be strings.
235237
- returns: <[Promise]<[Page]>>
236238

237239
Creates a new page in a new browser context. Closing this page will close the context as well.
@@ -271,6 +273,7 @@ await context.close();
271273
- [browserContext.setCookies(cookies)](#browsercontextsetcookiescookies)
272274
- [browserContext.setDefaultNavigationTimeout(timeout)](#browsercontextsetdefaultnavigationtimeouttimeout)
273275
- [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout)
276+
- [browserContext.setExtraHTTPHeaders(headers)](#browsercontextsetextrahttpheadersheaders)
274277
- [browserContext.setGeolocation(geolocation)](#browsercontextsetgeolocationgeolocation)
275278
- [browserContext.setPermissions(origin, permissions[])](#browsercontextsetpermissionsorigin-permissions)
276279
<!-- GEN:stop -->
@@ -374,6 +377,14 @@ This setting will change the default maximum time for all the methods accepting
374377

375378
> **NOTE** [`page.setDefaultNavigationTimeout`](#pagesetdefaultnavigationtimeouttimeout), [`page.setDefaultTimeout`](#pagesetdefaulttimeouttimeout) and [`browserContext.setDefaultNavigationTimeout`](#browsercontextsetdefaultnavigationtimeouttimeout) take priority over [`browserContext.setDefaultTimeout`](#browserContextsetdefaulttimeouttimeout).
376379
380+
#### browserContext.setExtraHTTPHeaders(headers)
381+
- `headers` <[Object]> An object containing additional HTTP headers to be sent with every request. All header values must be strings.
382+
- returns: <[Promise]>
383+
384+
The extra HTTP headers will be sent with every request initiated by any page in the context. These headers are merged with page-specific extra HTTP headers set with [page.setExtraHTTPHeaders()](#pagesetextrahttpheadersheaders). If page overrides a particular header, page-specific header value will be used instead of the browser context header value.
385+
386+
> **NOTE** `browserContext.setExtraHTTPHeaders` does not guarantee the order of headers in the outgoing requests.
387+
377388
#### browserContext.setGeolocation(geolocation)
378389
- `geolocation` <[Object]>
379390
- `latitude` <[number]> Latitude between -90 and 90.
@@ -3582,6 +3593,7 @@ const backgroundPage = await backroundPageTarget.page();
35823593
- [browserContext.setCookies(cookies)](#browsercontextsetcookiescookies)
35833594
- [browserContext.setDefaultNavigationTimeout(timeout)](#browsercontextsetdefaultnavigationtimeouttimeout)
35843595
- [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout)
3596+
- [browserContext.setExtraHTTPHeaders(headers)](#browsercontextsetextrahttpheadersheaders)
35853597
- [browserContext.setGeolocation(geolocation)](#browsercontextsetgeolocationgeolocation)
35863598
- [browserContext.setPermissions(origin, permissions[])](#browsercontextsetpermissionsorigin-permissions)
35873599
<!-- GEN:stop -->

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"main": "index.js",
1010
"playwright": {
1111
"chromium_revision": "744254",
12-
"firefox_revision": "1029",
12+
"firefox_revision": "1031",
1313
"webkit_revision": "1155"
1414
},
1515
"scripts": {

src/browserContext.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ export type BrowserContextOptions = {
3030
locale?: string,
3131
timezoneId?: string,
3232
geolocation?: types.Geolocation,
33-
permissions?: { [key: string]: string[] };
33+
permissions?: { [key: string]: string[] },
34+
extraHTTPHeaders?: network.Headers,
3435
};
3536

3637
export interface BrowserContext {
@@ -44,6 +45,7 @@ export interface BrowserContext {
4445
setPermissions(origin: string, permissions: string[]): Promise<void>;
4546
clearPermissions(): Promise<void>;
4647
setGeolocation(geolocation: types.Geolocation | null): Promise<void>;
48+
setExtraHTTPHeaders(headers: network.Headers): Promise<void>;
4749
close(): Promise<void>;
4850

4951
_existingPages(): Page[];
@@ -67,6 +69,8 @@ export function validateBrowserContextOptions(options: BrowserContextOptions): B
6769
result.viewport = { ...result.viewport };
6870
if (result.geolocation)
6971
result.geolocation = verifyGeolocation(result.geolocation);
72+
if (result.extraHTTPHeaders)
73+
result.extraHTTPHeaders = network.verifyHeaders(result.extraHTTPHeaders);
7074
return result;
7175
}
7276

src/chromium/crBrowser.ts

+6
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,12 @@ export class CRBrowserContext extends platform.EventEmitter implements BrowserCo
294294
await (page._delegate as CRPage)._client.send('Emulation.setGeolocationOverride', geolocation || {});
295295
}
296296

297+
async setExtraHTTPHeaders(headers: network.Headers): Promise<void> {
298+
this._options.extraHTTPHeaders = network.verifyHeaders(headers);
299+
for (const page of this._existingPages())
300+
await (page._delegate as CRPage).updateExtraHTTPHeaders();
301+
}
302+
297303
async close() {
298304
if (this._closed)
299305
return;

src/chromium/crPage.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export class CRPage implements PageDelegate {
118118
promises.push(emulateTimezone(this._client, options.timezoneId));
119119
if (options.geolocation)
120120
promises.push(this._client.send('Emulation.setGeolocationOverride', options.geolocation));
121+
promises.push(this.updateExtraHTTPHeaders());
121122
await Promise.all(promises);
122123
}
123124

@@ -316,7 +317,11 @@ export class CRPage implements PageDelegate {
316317
this._page._onFileChooserOpened(handle);
317318
}
318319

319-
async setExtraHTTPHeaders(headers: network.Headers): Promise<void> {
320+
async updateExtraHTTPHeaders(): Promise<void> {
321+
const headers = network.mergeHeaders([
322+
this._page.context()._options.extraHTTPHeaders,
323+
this._page._state.extraHTTPHeaders
324+
]);
320325
await this._client.send('Network.setExtraHTTPHeaders', { headers });
321326
}
322327

src/firefox/ffBrowser.ts

+8
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import * as platform from '../platform';
2828
import { Protocol } from './protocol';
2929
import { ConnectionTransport, SlowMoTransport } from '../transport';
3030
import { TimeoutSettings } from '../timeoutSettings';
31+
import { headersArray } from './ffNetworkManager';
3132

3233
export class FFBrowser extends platform.EventEmitter implements Browser {
3334
_connection: FFConnection;
@@ -274,6 +275,8 @@ export class FFBrowserContext extends platform.EventEmitter implements BrowserCo
274275
await Promise.all(entries.map(entry => this.setPermissions(entry[0], entry[1])));
275276
if (this._options.geolocation)
276277
await this.setGeolocation(this._options.geolocation);
278+
if (this._options.extraHTTPHeaders)
279+
await this.setExtraHTTPHeaders(this._options.extraHTTPHeaders);
277280
}
278281

279282
_existingPages(): Page[] {
@@ -349,6 +352,11 @@ export class FFBrowserContext extends platform.EventEmitter implements BrowserCo
349352
throw new Error('Geolocation emulation is not supported in Firefox');
350353
}
351354

355+
async setExtraHTTPHeaders(headers: network.Headers): Promise<void> {
356+
this._options.extraHTTPHeaders = network.verifyHeaders(headers);
357+
await this._browser._connection.send('Browser.setExtraHTTPHeaders', { browserContextId: this._browserContextId || undefined, headers: headersArray(this._options.extraHTTPHeaders) });
358+
}
359+
352360
async close() {
353361
if (this._closed)
354362
return;

src/firefox/ffNetworkManager.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ class InterceptableRequest implements network.RequestDelegate {
208208
}
209209
}
210210

211-
function headersArray(headers: network.Headers): Protocol.Network.HTTPHeader[] {
211+
export function headersArray(headers: network.Headers): Protocol.Network.HTTPHeader[] {
212212
const result: Protocol.Network.HTTPHeader[] = [];
213213
for (const name in headers) {
214214
if (!Object.is(headers[name], undefined))

src/firefox/ffPage.ts

+3-7
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,13 @@ import * as dom from '../dom';
2121
import { FFSession } from './ffConnection';
2222
import { FFExecutionContext } from './ffExecutionContext';
2323
import { Page, PageDelegate, Worker } from '../page';
24-
import { FFNetworkManager } from './ffNetworkManager';
24+
import { FFNetworkManager, headersArray } from './ffNetworkManager';
2525
import { Events } from '../events';
2626
import * as dialog from '../dialog';
2727
import { Protocol } from './protocol';
2828
import { RawMouseImpl, RawKeyboardImpl } from './ffInput';
2929
import { BrowserContext } from '../browserContext';
3030
import { getAccessibilityTree } from './ffAccessibility';
31-
import * as network from '../network';
3231
import * as types from '../types';
3332
import * as platform from '../platform';
3433
import { kScreenshotDuringNavigationError } from '../screenshotter';
@@ -251,11 +250,8 @@ export class FFPage implements PageDelegate {
251250
return { newDocumentId: response.navigationId || undefined };
252251
}
253252

254-
async setExtraHTTPHeaders(headers: network.Headers): Promise<void> {
255-
const array = [];
256-
for (const [name, value] of Object.entries(headers))
257-
array.push({ name, value });
258-
await this._session.send('Network.setExtraHTTPHeaders', { headers: array });
253+
async updateExtraHTTPHeaders(): Promise<void> {
254+
await this._session.send('Network.setExtraHTTPHeaders', { headers: headersArray(this._page._state.extraHTTPHeaders || {}) });
259255
}
260256

261257
async setViewportSize(viewportSize: types.Size): Promise<void> {

src/network.ts

+29-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import * as frames from './frames';
18-
import { assert } from './helper';
18+
import { assert, helper } from './helper';
1919
import * as platform from './platform';
2020

2121
export type NetworkCookie = {
@@ -373,3 +373,31 @@ export const STATUS_TEXTS: { [status: string]: string } = {
373373
'510': 'Not Extended',
374374
'511': 'Network Authentication Required',
375375
};
376+
377+
export function verifyHeaders(headers: Headers): Headers {
378+
const result: Headers = {};
379+
for (const key of Object.keys(headers)) {
380+
const value = headers[key];
381+
assert(helper.isString(value), `Expected value of header "${key}" to be String, but "${typeof value}" is found.`);
382+
result[key] = value;
383+
}
384+
return result;
385+
}
386+
387+
export function mergeHeaders(headers: (Headers | undefined | null)[]): Headers {
388+
const lowerCaseToValue = new Map<string, string>();
389+
const lowerCaseToOriginalCase = new Map<string, string>();
390+
for (const h of headers) {
391+
if (!h)
392+
continue;
393+
for (const key of Object.keys(h)) {
394+
const lower = key.toLowerCase();
395+
lowerCaseToOriginalCase.set(lower, key);
396+
lowerCaseToValue.set(lower, h[key]);
397+
}
398+
}
399+
const result: Headers = {};
400+
for (const [lower, value] of lowerCaseToValue)
401+
result[lowerCaseToOriginalCase.get(lower)!] = value;
402+
return result;
403+
}

src/page.ts

+3-8
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export interface PageDelegate {
4545

4646
navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult>;
4747

48-
setExtraHTTPHeaders(extraHTTPHeaders: network.Headers): Promise<void>;
48+
updateExtraHTTPHeaders(): Promise<void>;
4949
setViewportSize(viewportSize: types.Size): Promise<void>;
5050
setEmulateMedia(mediaType: types.MediaType | null, colorScheme: types.ColorScheme | null): Promise<void>;
5151
setCacheEnabled(enabled: boolean): Promise<void>;
@@ -268,13 +268,8 @@ export class Page extends platform.EventEmitter {
268268
}
269269

270270
setExtraHTTPHeaders(headers: network.Headers) {
271-
this._state.extraHTTPHeaders = {};
272-
for (const key of Object.keys(headers)) {
273-
const value = headers[key];
274-
assert(helper.isString(value), `Expected value of header "${key}" to be String, but "${typeof value}" is found.`);
275-
this._state.extraHTTPHeaders[key] = value;
276-
}
277-
return this._delegate.setExtraHTTPHeaders(headers);
271+
this._state.extraHTTPHeaders = network.verifyHeaders(headers);
272+
return this._delegate.updateExtraHTTPHeaders();
278273
}
279274

280275
async _onBindingCalled(payload: string, context: js.ExecutionContext) {

src/webkit/wkBrowser.ts

+7
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { WKConnection, WKSession, kPageProxyMessageReceived, PageProxyMessageRec
2828
import { WKPageProxy } from './wkPageProxy';
2929
import * as platform from '../platform';
3030
import { TimeoutSettings } from '../timeoutSettings';
31+
import { WKPage } from './wkPage';
3132

3233
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.4 Safari/605.1.15';
3334

@@ -269,6 +270,12 @@ export class WKBrowserContext extends platform.EventEmitter implements BrowserCo
269270
await this._browser._browserSession.send('Browser.setGeolocationOverride', { browserContextId: this._browserContextId, geolocation: payload });
270271
}
271272

273+
async setExtraHTTPHeaders(headers: network.Headers): Promise<void> {
274+
this._options.extraHTTPHeaders = network.verifyHeaders(headers);
275+
for (const page of this._existingPages())
276+
await (page._delegate as WKPage).updateExtraHTTPHeaders();
277+
}
278+
272279
async close() {
273280
if (this._closed)
274281
return;

src/webkit/wkPage.ts

+12-10
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,7 @@ export class WKPage implements PageDelegate {
142142
}
143143
if (contextOptions.bypassCSP)
144144
promises.push(session.send('Page.setBypassCSP', { enabled: true }));
145-
if (this._page._state.extraHTTPHeaders || contextOptions.locale) {
146-
const headers = this._page._state.extraHTTPHeaders || {};
147-
if (contextOptions.locale)
148-
headers['Accept-Language'] = contextOptions.locale;
149-
promises.push(session.send('Network.setExtraHTTPHeaders', { headers }));
150-
}
145+
promises.push(session.send('Network.setExtraHTTPHeaders', { headers: this._calculateExtraHTTPHeaders() }));
151146
if (this._page._state.hasTouch)
152147
promises.push(session.send('Page.setTouchEmulationEnabled', { enabled: true }));
153148
if (contextOptions.timezoneId) {
@@ -378,12 +373,19 @@ export class WKPage implements PageDelegate {
378373
await Promise.all(promises);
379374
}
380375

381-
async setExtraHTTPHeaders(headers: network.Headers): Promise<void> {
382-
const copy = { ...headers };
376+
async updateExtraHTTPHeaders(): Promise<void> {
377+
await this._updateState('Network.setExtraHTTPHeaders', { headers: this._calculateExtraHTTPHeaders() });
378+
}
379+
380+
_calculateExtraHTTPHeaders(): network.Headers {
381+
const headers = network.mergeHeaders([
382+
this._page.context()._options.extraHTTPHeaders,
383+
this._page._state.extraHTTPHeaders
384+
]);
383385
const locale = this._page.context()._options.locale;
384386
if (locale)
385-
copy['Accept-Language'] = locale;
386-
await this._updateState('Network.setExtraHTTPHeaders', { headers: copy });
387+
headers['Accept-Language'] = locale;
388+
return headers;
387389
}
388390

389391
async setEmulateMedia(mediaType: types.MediaType | null, colorScheme: types.ColorScheme | null): Promise<void> {

test/network.spec.js

+29
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,35 @@ module.exports.describe = function({testRunner, expect, MAC, WIN, FFOX, CHROMIUM
331331
]);
332332
expect(request.headers['foo']).toBe('bar');
333333
});
334+
it('should work with extra headers from browser context', async({browser, server}) => {
335+
const context = await browser.newContext();
336+
await context.setExtraHTTPHeaders({
337+
'foo': 'bar',
338+
});
339+
const page = await context.newPage();
340+
const [request] = await Promise.all([
341+
server.waitForRequest('/empty.html'),
342+
page.goto(server.EMPTY_PAGE),
343+
]);
344+
await context.close();
345+
expect(request.headers['foo']).toBe('bar');
346+
});
347+
it('should override extra headers from browser context', async({browser, server}) => {
348+
const context = await browser.newContext({
349+
extraHTTPHeaders: { 'fOo': 'bAr', 'baR': 'foO' },
350+
});
351+
const page = await context.newPage();
352+
await page.setExtraHTTPHeaders({
353+
'Foo': 'Bar'
354+
});
355+
const [request] = await Promise.all([
356+
server.waitForRequest('/empty.html'),
357+
page.goto(server.EMPTY_PAGE),
358+
]);
359+
await context.close();
360+
expect(request.headers['foo']).toBe('Bar');
361+
expect(request.headers['bar']).toBe('foO');
362+
});
334363
it('should throw for non-string header values', async({page, server}) => {
335364
let error = null;
336365
try {

test/popup.spec.js

+12
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
3636
expect(userAgent).toBe('hey');
3737
expect(request.headers['user-agent']).toBe('hey');
3838
});
39+
it.skip(CHROMIUM)('should inherit extra headers from browser context', async function({browser, server}) {
40+
const context = await browser.newContext({
41+
extraHTTPHeaders: { 'foo': 'bar' },
42+
});
43+
const page = await context.newPage();
44+
await page.goto(server.EMPTY_PAGE);
45+
const requestPromise = server.waitForRequest('/dummy.html');
46+
await page.evaluate(url => window._popup = window.open(url), server.PREFIX + '/dummy.html');
47+
const request = await requestPromise;
48+
await context.close();
49+
expect(request.headers['foo']).toBe('bar');
50+
});
3951
it.skip(CHROMIUM)('should inherit touch support from browser context', async function({browser, server}) {
4052
const context = await browser.newContext({
4153
viewport: { width: 400, height: 500, isMobile: true }

0 commit comments

Comments
 (0)