Skip to content

Commit

Permalink
feat(fetch): sendImmediately
Browse files Browse the repository at this point in the history
  • Loading branch information
yury-s committed May 2, 2024
1 parent 980f9c6 commit 2a72a8f
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 4 deletions.
1 change: 1 addition & 0 deletions docs/src/api/params.md
Expand Up @@ -571,6 +571,7 @@ Whether to emulate network being offline. Defaults to `false`. Learn more about
- `username` <[string]>
- `password` <[string]>
- `origin` ?<[string]> Restrain sending http credentials on specific origin (scheme://host:port).
- `sendImmediately` ?<[boolean]> Whether to send `Authorization` header with the first API request. By deafult, the credentials are sent only when 401 (Unauthorized) response with `WWW-Authenticate` header is received. This option does not affect requests sent from the browser.

Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication).
If no origin is specified, the username and password are sent to any servers upon unauthorized responses.
Expand Down
5 changes: 5 additions & 0 deletions packages/playwright-core/src/protocol/validator.ts
Expand Up @@ -332,6 +332,7 @@ scheme.PlaywrightNewRequestParams = tObject({
username: tString,
password: tString,
origin: tOptional(tString),
sendImmediately: tOptional(tBoolean),
})),
proxy: tOptional(tObject({
server: tString,
Expand Down Expand Up @@ -545,6 +546,7 @@ scheme.BrowserTypeLaunchPersistentContextParams = tObject({
username: tString,
password: tString,
origin: tOptional(tString),
sendImmediately: tOptional(tBoolean),
})),
deviceScaleFactor: tOptional(tNumber),
isMobile: tOptional(tBoolean),
Expand Down Expand Up @@ -623,6 +625,7 @@ scheme.BrowserNewContextParams = tObject({
username: tString,
password: tString,
origin: tOptional(tString),
sendImmediately: tOptional(tBoolean),
})),
deviceScaleFactor: tOptional(tNumber),
isMobile: tOptional(tBoolean),
Expand Down Expand Up @@ -684,6 +687,7 @@ scheme.BrowserNewContextForReuseParams = tObject({
username: tString,
password: tString,
origin: tOptional(tString),
sendImmediately: tOptional(tBoolean),
})),
deviceScaleFactor: tOptional(tNumber),
isMobile: tOptional(tBoolean),
Expand Down Expand Up @@ -2474,6 +2478,7 @@ scheme.AndroidDeviceLaunchBrowserParams = tObject({
username: tString,
password: tString,
origin: tOptional(tString),
sendImmediately: tOptional(tBoolean),
})),
deviceScaleFactor: tOptional(tNumber),
isMobile: tOptional(tBoolean),
Expand Down
16 changes: 12 additions & 4 deletions packages/playwright-core/src/server/fetch.ts
Expand Up @@ -158,6 +158,10 @@ export abstract class APIRequestContext extends SdkObject {
requestUrl.searchParams.set(name, value);
}

const credentials = this._getHttpCredentials(requestUrl);
if (credentials?.sendImmediately)
setBasicAuthorizationHeader(headers, credentials);

const method = params.method?.toUpperCase() || 'GET';
const proxy = defaults.proxy;
let agent;
Expand Down Expand Up @@ -355,9 +359,7 @@ export abstract class APIRequestContext extends SdkObject {
const auth = response.headers['www-authenticate'];
const credentials = this._getHttpCredentials(url);
if (auth?.trim().startsWith('Basic') && credentials) {
const { username, password } = credentials;
const encoded = Buffer.from(`${username || ''}:${password || ''}`).toString('base64');
setHeader(options.headers, 'authorization', `Basic ${encoded}`);
setBasicAuthorizationHeader(options.headers, credentials);
notifyRequestFinished();
fulfill(this._sendRequest(progress, url, options, postData));
request.destroy();
Expand Down Expand Up @@ -729,4 +731,10 @@ function shouldBypassProxy(url: URL, bypass?: string): boolean {
});
const domain = '.' + url.hostname;
return domains.some(d => domain.endsWith(d));
}
}

function setBasicAuthorizationHeader(headers: { [name: string]: string }, credentials: HTTPCredentials) {
const { username, password } = credentials;
const encoded = Buffer.from(`${username || ''}:${password || ''}`).toString('base64');
setHeader(headers, 'authorization', `Basic ${encoded}`);
}
42 changes: 42 additions & 0 deletions packages/playwright-core/types/types.d.ts
Expand Up @@ -13376,6 +13376,13 @@ export interface BrowserType<Unused = {}> {
* Restrain sending http credentials on specific origin (scheme://host:port).
*/
origin?: string;

/**
* Whether to send `Authorization` header with the first API request. By deafult, the credentials are sent only when
* 401 (Unauthorized) response with `WWW-Authenticate` header is received. This option does not affect requests sent
* from the browser.
*/
sendImmediately?: boolean;
};

/**
Expand Down Expand Up @@ -14892,6 +14899,13 @@ export interface AndroidDevice {
* Restrain sending http credentials on specific origin (scheme://host:port).
*/
origin?: string;

/**
* Whether to send `Authorization` header with the first API request. By deafult, the credentials are sent only when
* 401 (Unauthorized) response with `WWW-Authenticate` header is received. This option does not affect requests sent
* from the browser.
*/
sendImmediately?: boolean;
};

/**
Expand Down Expand Up @@ -15616,6 +15630,13 @@ export interface APIRequest {
* Restrain sending http credentials on specific origin (scheme://host:port).
*/
origin?: string;

/**
* Whether to send `Authorization` header with the first API request. By deafult, the credentials are sent only when
* 401 (Unauthorized) response with `WWW-Authenticate` header is received. This option does not affect requests sent
* from the browser.
*/
sendImmediately?: boolean;
};

/**
Expand Down Expand Up @@ -16760,6 +16781,13 @@ export interface Browser extends EventEmitter {
* Restrain sending http credentials on specific origin (scheme://host:port).
*/
origin?: string;

/**
* Whether to send `Authorization` header with the first API request. By deafult, the credentials are sent only when
* 401 (Unauthorized) response with `WWW-Authenticate` header is received. This option does not affect requests sent
* from the browser.
*/
sendImmediately?: boolean;
};

/**
Expand Down Expand Up @@ -17647,6 +17675,13 @@ export interface Electron {
* Restrain sending http credentials on specific origin (scheme://host:port).
*/
origin?: string;

/**
* Whether to send `Authorization` header with the first API request. By deafult, the credentials are sent only when
* 401 (Unauthorized) response with `WWW-Authenticate` header is received. This option does not affect requests sent
* from the browser.
*/
sendImmediately?: boolean;
};

/**
Expand Down Expand Up @@ -20307,6 +20342,13 @@ export interface HTTPCredentials {
* Restrain sending http credentials on specific origin (scheme://host:port).
*/
origin?: string;

/**
* Whether to send `Authorization` header with the first API request. By deafult, the credentials are sent only when
* 401 (Unauthorized) response with `WWW-Authenticate` header is received. This option does not affect requests sent
* from the browser.
*/
sendImmediately?: boolean;
}

export interface Geolocation {
Expand Down
10 changes: 10 additions & 0 deletions packages/protocol/src/channels.ts
Expand Up @@ -574,6 +574,7 @@ export type PlaywrightNewRequestParams = {
username: string,
password: string,
origin?: string,
sendImmediately?: boolean,
},
proxy?: {
server: string,
Expand All @@ -597,6 +598,7 @@ export type PlaywrightNewRequestOptions = {
username: string,
password: string,
origin?: string,
sendImmediately?: boolean,
},
proxy?: {
server: string,
Expand Down Expand Up @@ -953,6 +955,7 @@ export type BrowserTypeLaunchPersistentContextParams = {
username: string,
password: string,
origin?: string,
sendImmediately?: boolean,
},
deviceScaleFactor?: number,
isMobile?: boolean,
Expand Down Expand Up @@ -1025,6 +1028,7 @@ export type BrowserTypeLaunchPersistentContextOptions = {
username: string,
password: string,
origin?: string,
sendImmediately?: boolean,
},
deviceScaleFactor?: number,
isMobile?: boolean,
Expand Down Expand Up @@ -1132,6 +1136,7 @@ export type BrowserNewContextParams = {
username: string,
password: string,
origin?: string,
sendImmediately?: boolean,
},
deviceScaleFactor?: number,
isMobile?: boolean,
Expand Down Expand Up @@ -1190,6 +1195,7 @@ export type BrowserNewContextOptions = {
username: string,
password: string,
origin?: string,
sendImmediately?: boolean,
},
deviceScaleFactor?: number,
isMobile?: boolean,
Expand Down Expand Up @@ -1251,6 +1257,7 @@ export type BrowserNewContextForReuseParams = {
username: string,
password: string,
origin?: string,
sendImmediately?: boolean,
},
deviceScaleFactor?: number,
isMobile?: boolean,
Expand Down Expand Up @@ -1309,6 +1316,7 @@ export type BrowserNewContextForReuseOptions = {
username: string,
password: string,
origin?: string,
sendImmediately?: boolean,
},
deviceScaleFactor?: number,
isMobile?: boolean,
Expand Down Expand Up @@ -4471,6 +4479,7 @@ export type AndroidDeviceLaunchBrowserParams = {
username: string,
password: string,
origin?: string,
sendImmediately?: boolean,
},
deviceScaleFactor?: number,
isMobile?: boolean,
Expand Down Expand Up @@ -4527,6 +4536,7 @@ export type AndroidDeviceLaunchBrowserOptions = {
username: string,
password: string,
origin?: string,
sendImmediately?: boolean,
},
deviceScaleFactor?: number,
isMobile?: boolean,
Expand Down
2 changes: 2 additions & 0 deletions packages/protocol/src/protocol.yml
Expand Up @@ -454,6 +454,7 @@ ContextOptions:
username: string
password: string
origin: string?
sendImmediately: boolean?
deviceScaleFactor: number?
isMobile: boolean?
hasTouch: boolean?
Expand Down Expand Up @@ -671,6 +672,7 @@ Playwright:
username: string
password: string
origin: string?
sendImmediately: boolean?
proxy:
type: object?
properties:
Expand Down
24 changes: 24 additions & 0 deletions tests/library/browsercontext-fetch.spec.ts
Expand Up @@ -421,6 +421,30 @@ it('should return error with wrong credentials', async ({ context, server }) =>
expect(response2.status()).toBe(401);
});

it('should support HTTPCredentials.sendImmediately', async ({ contextFactory, server }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30534' });
const context = await contextFactory({
httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX.toUpperCase(), sendImmediately: true }
});
{
const [serverRequest, response] = await Promise.all([
server.waitForRequest('/empty.html'),
context.request.get(server.EMPTY_PAGE)
]);
expect(serverRequest.headers.authorization).toBe('Basic ' + Buffer.from('user:pass').toString('base64'));
expect(response.status()).toBe(200);
}
{
const [serverRequest, response] = await Promise.all([
server.waitForRequest('/empty.html'),
context.request.get(server.CROSS_PROCESS_PREFIX + '/empty.html')
]);
// Not sent to another origin.
expect(serverRequest.headers.authorization).toBe(undefined);
expect(response.status()).toBe(200);
}
});

it('delete should support post data', async ({ context, server }) => {
const [request, response] = await Promise.all([
server.waitForRequest('/simple.json'),
Expand Down
24 changes: 24 additions & 0 deletions tests/library/global-fetch.spec.ts
Expand Up @@ -154,6 +154,30 @@ it('should support WWW-Authenticate: Basic', async ({ playwright, server }) => {
expect(credentials).toBe('user:pass');
});

it('should support HTTPCredentials.sendImmediately', async ({ playwright, server }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30534' });
const request = await playwright.request.newContext({
httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX.toUpperCase(), sendImmediately: true }
});
{
const [serverRequest, response] = await Promise.all([
server.waitForRequest('/empty.html'),
request.get(server.EMPTY_PAGE)
]);
expect(serverRequest.headers.authorization).toBe('Basic ' + Buffer.from('user:pass').toString('base64'));
expect(response.status()).toBe(200);
}
{
const [serverRequest, response] = await Promise.all([
server.waitForRequest('/empty.html'),
request.get(server.CROSS_PROCESS_PREFIX + '/empty.html')
]);
// Not sent to another origin.
expect(serverRequest.headers.authorization).toBe(undefined);
expect(response.status()).toBe(200);
}
});

it('should support global ignoreHTTPSErrors option', async ({ playwright, httpsServer }) => {
const request = await playwright.request.newContext({ ignoreHTTPSErrors: true });
const response = await request.get(httpsServer.EMPTY_PAGE);
Expand Down

0 comments on commit 2a72a8f

Please sign in to comment.