feat(fetch): sendImmediately (#30627)
Fixes https://github.com/microsoft/playwright/issues/30534
This commit is contained in:
parent
5639cab4a4
commit
d5b387159a
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
@ -730,3 +732,9 @@ 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}`);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
42
packages/playwright-core/types/types.d.ts
vendored
42
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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'),
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue