feat(fetch): sendImmediately (#30627)

Fixes https://github.com/microsoft/playwright/issues/30534
This commit is contained in:
Yury Semikhatsky 2024-05-02 16:30:12 -07:00 committed by GitHub
parent 5639cab4a4
commit d5b387159a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 127 additions and 5 deletions

View file

@ -571,6 +571,7 @@ Whether to emulate network being offline. Defaults to `false`. Learn more about
- `username` <[string]> - `username` <[string]>
- `password` <[string]> - `password` <[string]>
- `origin` ?<[string]> Restrain sending http credentials on specific origin (scheme://host:port). - `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). 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. If no origin is specified, the username and password are sent to any servers upon unauthorized responses.

View file

@ -332,6 +332,7 @@ scheme.PlaywrightNewRequestParams = tObject({
username: tString, username: tString,
password: tString, password: tString,
origin: tOptional(tString), origin: tOptional(tString),
sendImmediately: tOptional(tBoolean),
})), })),
proxy: tOptional(tObject({ proxy: tOptional(tObject({
server: tString, server: tString,
@ -545,6 +546,7 @@ scheme.BrowserTypeLaunchPersistentContextParams = tObject({
username: tString, username: tString,
password: tString, password: tString,
origin: tOptional(tString), origin: tOptional(tString),
sendImmediately: tOptional(tBoolean),
})), })),
deviceScaleFactor: tOptional(tNumber), deviceScaleFactor: tOptional(tNumber),
isMobile: tOptional(tBoolean), isMobile: tOptional(tBoolean),
@ -623,6 +625,7 @@ scheme.BrowserNewContextParams = tObject({
username: tString, username: tString,
password: tString, password: tString,
origin: tOptional(tString), origin: tOptional(tString),
sendImmediately: tOptional(tBoolean),
})), })),
deviceScaleFactor: tOptional(tNumber), deviceScaleFactor: tOptional(tNumber),
isMobile: tOptional(tBoolean), isMobile: tOptional(tBoolean),
@ -684,6 +687,7 @@ scheme.BrowserNewContextForReuseParams = tObject({
username: tString, username: tString,
password: tString, password: tString,
origin: tOptional(tString), origin: tOptional(tString),
sendImmediately: tOptional(tBoolean),
})), })),
deviceScaleFactor: tOptional(tNumber), deviceScaleFactor: tOptional(tNumber),
isMobile: tOptional(tBoolean), isMobile: tOptional(tBoolean),
@ -2474,6 +2478,7 @@ scheme.AndroidDeviceLaunchBrowserParams = tObject({
username: tString, username: tString,
password: tString, password: tString,
origin: tOptional(tString), origin: tOptional(tString),
sendImmediately: tOptional(tBoolean),
})), })),
deviceScaleFactor: tOptional(tNumber), deviceScaleFactor: tOptional(tNumber),
isMobile: tOptional(tBoolean), isMobile: tOptional(tBoolean),

View file

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

View file

@ -344,7 +344,12 @@ export class FFBrowserContext extends BrowserContext {
async doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise<void> { async doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise<void> {
this._options.httpCredentials = httpCredentials; this._options.httpCredentials = httpCredentials;
await this._browser.session.send('Browser.setHTTPCredentials', { browserContextId: this._browserContextId, credentials: httpCredentials || null }); let credentials = null;
if (httpCredentials) {
const { username, password, origin } = httpCredentials;
credentials = { username, password, origin };
}
await this._browser.session.send('Browser.setHTTPCredentials', { browserContextId: this._browserContextId, credentials });
} }
async doAddInitScript(source: string) { async doAddInitScript(source: string) {

View file

@ -58,6 +58,7 @@ export type Credentials = {
username: string; username: string;
password: string; password: string;
origin?: string; origin?: string;
sendImmediately?: boolean;
}; };
export type Geolocation = { export type Geolocation = {

View file

@ -13376,6 +13376,13 @@ export interface BrowserType<Unused = {}> {
* Restrain sending http credentials on specific origin (scheme://host:port). * Restrain sending http credentials on specific origin (scheme://host:port).
*/ */
origin?: string; 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;
}; };
/** /**
@ -14892,6 +14899,13 @@ export interface AndroidDevice {
* Restrain sending http credentials on specific origin (scheme://host:port). * Restrain sending http credentials on specific origin (scheme://host:port).
*/ */
origin?: string; 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;
}; };
/** /**
@ -15616,6 +15630,13 @@ export interface APIRequest {
* Restrain sending http credentials on specific origin (scheme://host:port). * Restrain sending http credentials on specific origin (scheme://host:port).
*/ */
origin?: string; 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;
}; };
/** /**
@ -16760,6 +16781,13 @@ export interface Browser extends EventEmitter {
* Restrain sending http credentials on specific origin (scheme://host:port). * Restrain sending http credentials on specific origin (scheme://host:port).
*/ */
origin?: string; 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;
}; };
/** /**
@ -17647,6 +17675,13 @@ export interface Electron {
* Restrain sending http credentials on specific origin (scheme://host:port). * Restrain sending http credentials on specific origin (scheme://host:port).
*/ */
origin?: string; 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;
}; };
/** /**
@ -20307,6 +20342,13 @@ export interface HTTPCredentials {
* Restrain sending http credentials on specific origin (scheme://host:port). * Restrain sending http credentials on specific origin (scheme://host:port).
*/ */
origin?: string; 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 { export interface Geolocation {

View file

@ -574,6 +574,7 @@ export type PlaywrightNewRequestParams = {
username: string, username: string,
password: string, password: string,
origin?: string, origin?: string,
sendImmediately?: boolean,
}, },
proxy?: { proxy?: {
server: string, server: string,
@ -597,6 +598,7 @@ export type PlaywrightNewRequestOptions = {
username: string, username: string,
password: string, password: string,
origin?: string, origin?: string,
sendImmediately?: boolean,
}, },
proxy?: { proxy?: {
server: string, server: string,
@ -953,6 +955,7 @@ export type BrowserTypeLaunchPersistentContextParams = {
username: string, username: string,
password: string, password: string,
origin?: string, origin?: string,
sendImmediately?: boolean,
}, },
deviceScaleFactor?: number, deviceScaleFactor?: number,
isMobile?: boolean, isMobile?: boolean,
@ -1025,6 +1028,7 @@ export type BrowserTypeLaunchPersistentContextOptions = {
username: string, username: string,
password: string, password: string,
origin?: string, origin?: string,
sendImmediately?: boolean,
}, },
deviceScaleFactor?: number, deviceScaleFactor?: number,
isMobile?: boolean, isMobile?: boolean,
@ -1132,6 +1136,7 @@ export type BrowserNewContextParams = {
username: string, username: string,
password: string, password: string,
origin?: string, origin?: string,
sendImmediately?: boolean,
}, },
deviceScaleFactor?: number, deviceScaleFactor?: number,
isMobile?: boolean, isMobile?: boolean,
@ -1190,6 +1195,7 @@ export type BrowserNewContextOptions = {
username: string, username: string,
password: string, password: string,
origin?: string, origin?: string,
sendImmediately?: boolean,
}, },
deviceScaleFactor?: number, deviceScaleFactor?: number,
isMobile?: boolean, isMobile?: boolean,
@ -1251,6 +1257,7 @@ export type BrowserNewContextForReuseParams = {
username: string, username: string,
password: string, password: string,
origin?: string, origin?: string,
sendImmediately?: boolean,
}, },
deviceScaleFactor?: number, deviceScaleFactor?: number,
isMobile?: boolean, isMobile?: boolean,
@ -1309,6 +1316,7 @@ export type BrowserNewContextForReuseOptions = {
username: string, username: string,
password: string, password: string,
origin?: string, origin?: string,
sendImmediately?: boolean,
}, },
deviceScaleFactor?: number, deviceScaleFactor?: number,
isMobile?: boolean, isMobile?: boolean,
@ -4471,6 +4479,7 @@ export type AndroidDeviceLaunchBrowserParams = {
username: string, username: string,
password: string, password: string,
origin?: string, origin?: string,
sendImmediately?: boolean,
}, },
deviceScaleFactor?: number, deviceScaleFactor?: number,
isMobile?: boolean, isMobile?: boolean,
@ -4527,6 +4536,7 @@ export type AndroidDeviceLaunchBrowserOptions = {
username: string, username: string,
password: string, password: string,
origin?: string, origin?: string,
sendImmediately?: boolean,
}, },
deviceScaleFactor?: number, deviceScaleFactor?: number,
isMobile?: boolean, isMobile?: boolean,

View file

@ -454,6 +454,7 @@ ContextOptions:
username: string username: string
password: string password: string
origin: string? origin: string?
sendImmediately: boolean?
deviceScaleFactor: number? deviceScaleFactor: number?
isMobile: boolean? isMobile: boolean?
hasTouch: boolean? hasTouch: boolean?
@ -671,6 +672,7 @@ Playwright:
username: string username: string
password: string password: string
origin: string? origin: string?
sendImmediately: boolean?
proxy: proxy:
type: object? type: object?
properties: properties:

View file

@ -421,6 +421,30 @@ it('should return error with wrong credentials', async ({ context, server }) =>
expect(response2.status()).toBe(401); 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 }) => { it('delete should support post data', async ({ context, server }) => {
const [request, response] = await Promise.all([ const [request, response] = await Promise.all([
server.waitForRequest('/simple.json'), server.waitForRequest('/simple.json'),

View file

@ -154,6 +154,30 @@ it('should support WWW-Authenticate: Basic', async ({ playwright, server }) => {
expect(credentials).toBe('user:pass'); 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 }) => { it('should support global ignoreHTTPSErrors option', async ({ playwright, httpsServer }) => {
const request = await playwright.request.newContext({ ignoreHTTPSErrors: true }); const request = await playwright.request.newContext({ ignoreHTTPSErrors: true });
const response = await request.get(httpsServer.EMPTY_PAGE); const response = await request.get(httpsServer.EMPTY_PAGE);