parent
dab61df451
commit
0794cb1486
|
|
@ -50,10 +50,12 @@ type FetchRequestOptions = {
|
||||||
baseURL?: string;
|
baseURL?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type HeadersObject = Readonly<{ [name: string]: string }>;
|
||||||
|
|
||||||
export type APIRequestEvent = {
|
export type APIRequestEvent = {
|
||||||
url: URL,
|
url: URL,
|
||||||
method: string,
|
method: string,
|
||||||
headers: { [name: string]: string },
|
headers: HeadersObject,
|
||||||
cookies: channels.NameValue[],
|
cookies: channels.NameValue[],
|
||||||
postData?: Buffer
|
postData?: Buffer
|
||||||
};
|
};
|
||||||
|
|
@ -72,6 +74,7 @@ export type APIRequestFinishedEvent = {
|
||||||
type SendRequestOptions = https.RequestOptions & {
|
type SendRequestOptions = https.RequestOptions & {
|
||||||
maxRedirects: number,
|
maxRedirects: number,
|
||||||
deadline: number,
|
deadline: number,
|
||||||
|
headers: HeadersObject,
|
||||||
__testHookLookup?: (hostname: string) => LookupAddress[]
|
__testHookLookup?: (hostname: string) => LookupAddress[]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -130,20 +133,21 @@ export abstract class APIRequestContext extends SdkObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetch(params: channels.APIRequestContextFetchParams, metadata: CallMetadata): Promise<channels.APIResponse> {
|
async fetch(params: channels.APIRequestContextFetchParams, metadata: CallMetadata): Promise<channels.APIResponse> {
|
||||||
const headers: { [name: string]: string } = {};
|
|
||||||
const defaults = this._defaultOptions();
|
const defaults = this._defaultOptions();
|
||||||
headers['user-agent'] = defaults.userAgent;
|
const headers: HeadersObject = {
|
||||||
headers['accept'] = '*/*';
|
'user-agent': defaults.userAgent,
|
||||||
headers['accept-encoding'] = 'gzip,deflate,br';
|
'accept': '*/*',
|
||||||
|
'accept-encoding': 'gzip,deflate,br',
|
||||||
|
};
|
||||||
|
|
||||||
if (defaults.extraHTTPHeaders) {
|
if (defaults.extraHTTPHeaders) {
|
||||||
for (const { name, value } of defaults.extraHTTPHeaders)
|
for (const { name, value } of defaults.extraHTTPHeaders)
|
||||||
headers[name.toLowerCase()] = value;
|
setHeader(headers, name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.headers) {
|
if (params.headers) {
|
||||||
for (const { name, value } of params.headers)
|
for (const { name, value } of params.headers)
|
||||||
headers[name.toLowerCase()] = value;
|
setHeader(headers, name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const method = params.method?.toUpperCase() || 'GET';
|
const method = params.method?.toUpperCase() || 'GET';
|
||||||
|
|
@ -188,7 +192,7 @@ export abstract class APIRequestContext extends SdkObject {
|
||||||
|
|
||||||
const postData = serializePostData(params, headers);
|
const postData = serializePostData(params, headers);
|
||||||
if (postData)
|
if (postData)
|
||||||
headers['content-length'] = String(postData.byteLength);
|
setHeader(headers, 'content-length', String(postData.byteLength));
|
||||||
const controller = new ProgressController(metadata, this);
|
const controller = new ProgressController(metadata, this);
|
||||||
const fetchResponse = await controller.run(progress => {
|
const fetchResponse = await controller.run(progress => {
|
||||||
return this._sendRequest(progress, requestUrl, options, postData);
|
return this._sendRequest(progress, requestUrl, options, postData);
|
||||||
|
|
@ -227,27 +231,27 @@ export abstract class APIRequestContext extends SdkObject {
|
||||||
return cookies;
|
return cookies;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _updateRequestCookieHeader(url: URL, options: http.RequestOptions) {
|
private async _updateRequestCookieHeader(url: URL, headers: HeadersObject) {
|
||||||
if (options.headers!['cookie'] !== undefined)
|
if (getHeader(headers, 'cookie') !== undefined)
|
||||||
return;
|
return;
|
||||||
const cookies = await this._cookies(url);
|
const cookies = await this._cookies(url);
|
||||||
if (cookies.length) {
|
if (cookies.length) {
|
||||||
const valueArray = cookies.map(c => `${c.name}=${c.value}`);
|
const valueArray = cookies.map(c => `${c.name}=${c.value}`);
|
||||||
options.headers!['cookie'] = valueArray.join('; ');
|
setHeader(headers, 'cookie', valueArray.join('; '));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _sendRequest(progress: Progress, url: URL, options: SendRequestOptions, postData?: Buffer): Promise<Omit<channels.APIResponse, 'fetchUid'> & { body: Buffer }>{
|
private async _sendRequest(progress: Progress, url: URL, options: SendRequestOptions, postData?: Buffer): Promise<Omit<channels.APIResponse, 'fetchUid'> & { body: Buffer }>{
|
||||||
await this._updateRequestCookieHeader(url, options);
|
await this._updateRequestCookieHeader(url, options.headers);
|
||||||
|
|
||||||
const requestCookies = (options.headers!['cookie'] as (string | undefined))?.split(';').map(p => {
|
const requestCookies = getHeader(options.headers, 'cookie')?.split(';').map(p => {
|
||||||
const [name, value] = p.split('=').map(v => v.trim());
|
const [name, value] = p.split('=').map(v => v.trim());
|
||||||
return { name, value };
|
return { name, value };
|
||||||
}) || [];
|
}) || [];
|
||||||
const requestEvent: APIRequestEvent = {
|
const requestEvent: APIRequestEvent = {
|
||||||
url,
|
url,
|
||||||
method: options.method!,
|
method: options.method!,
|
||||||
headers: options.headers as { [name: string]: string },
|
headers: options.headers,
|
||||||
cookies: requestCookies,
|
cookies: requestCookies,
|
||||||
postData
|
postData
|
||||||
};
|
};
|
||||||
|
|
@ -287,8 +291,8 @@ export abstract class APIRequestContext extends SdkObject {
|
||||||
request.destroy();
|
request.destroy();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const headers = { ...options.headers };
|
const headers: HeadersObject = { ...options.headers };
|
||||||
delete headers[`cookie`];
|
removeHeader(headers, `cookie`);
|
||||||
|
|
||||||
// HTTP-redirect fetch step 13 (https://fetch.spec.whatwg.org/#http-redirect-fetch)
|
// HTTP-redirect fetch step 13 (https://fetch.spec.whatwg.org/#http-redirect-fetch)
|
||||||
const status = response.statusCode!;
|
const status = response.statusCode!;
|
||||||
|
|
@ -297,11 +301,11 @@ export abstract class APIRequestContext extends SdkObject {
|
||||||
status === 303 && !['GET', 'HEAD'].includes(method)) {
|
status === 303 && !['GET', 'HEAD'].includes(method)) {
|
||||||
method = 'GET';
|
method = 'GET';
|
||||||
postData = undefined;
|
postData = undefined;
|
||||||
delete headers[`content-encoding`];
|
removeHeader(headers, `content-encoding`);
|
||||||
delete headers[`content-language`];
|
removeHeader(headers, `content-language`);
|
||||||
delete headers[`content-length`];
|
removeHeader(headers, `content-length`);
|
||||||
delete headers[`content-location`];
|
removeHeader(headers, `content-location`);
|
||||||
delete headers[`content-type`];
|
removeHeader(headers, `content-type`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const redirectOptions: SendRequestOptions = {
|
const redirectOptions: SendRequestOptions = {
|
||||||
|
|
@ -333,13 +337,13 @@ export abstract class APIRequestContext extends SdkObject {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (response.statusCode === 401 && !options.headers!['authorization']) {
|
if (response.statusCode === 401 && !getHeader(options.headers, 'authorization')) {
|
||||||
const auth = response.headers['www-authenticate'];
|
const auth = response.headers['www-authenticate'];
|
||||||
const credentials = this._defaultOptions().httpCredentials;
|
const credentials = this._defaultOptions().httpCredentials;
|
||||||
if (auth?.trim().startsWith('Basic') && credentials) {
|
if (auth?.trim().startsWith('Basic') && credentials) {
|
||||||
const { username, password } = credentials;
|
const { username, password } = credentials;
|
||||||
const encoded = Buffer.from(`${username || ''}:${password || ''}`).toString('base64');
|
const encoded = Buffer.from(`${username || ''}:${password || ''}`).toString('base64');
|
||||||
options.headers!['authorization'] = `Basic ${encoded}`;
|
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();
|
||||||
|
|
@ -651,17 +655,17 @@ function isJsonParsable(value: any) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function serializePostData(params: channels.APIRequestContextFetchParams, headers: { [name: string]: string }): Buffer | undefined {
|
function serializePostData(params: channels.APIRequestContextFetchParams, headers: HeadersObject): Buffer | undefined {
|
||||||
assert((params.postData ? 1 : 0) + (params.jsonData ? 1 : 0) + (params.formData ? 1 : 0) + (params.multipartData ? 1 : 0) <= 1, `Only one of 'data', 'form' or 'multipart' can be specified`);
|
assert((params.postData ? 1 : 0) + (params.jsonData ? 1 : 0) + (params.formData ? 1 : 0) + (params.multipartData ? 1 : 0) <= 1, `Only one of 'data', 'form' or 'multipart' can be specified`);
|
||||||
if (params.jsonData !== undefined) {
|
if (params.jsonData !== undefined) {
|
||||||
const json = isJsonParsable(params.jsonData) ? params.jsonData : JSON.stringify(params.jsonData);
|
const json = isJsonParsable(params.jsonData) ? params.jsonData : JSON.stringify(params.jsonData);
|
||||||
headers['content-type'] ??= 'application/json';
|
setHeader(headers, 'content-type', 'application/json', true);
|
||||||
return Buffer.from(json, 'utf8');
|
return Buffer.from(json, 'utf8');
|
||||||
} else if (params.formData) {
|
} else if (params.formData) {
|
||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams();
|
||||||
for (const { name, value } of params.formData)
|
for (const { name, value } of params.formData)
|
||||||
searchParams.append(name, value);
|
searchParams.append(name, value);
|
||||||
headers['content-type'] ??= 'application/x-www-form-urlencoded';
|
setHeader(headers, 'content-type', 'application/x-www-form-urlencoded', true);
|
||||||
return Buffer.from(searchParams.toString(), 'utf8');
|
return Buffer.from(searchParams.toString(), 'utf8');
|
||||||
} else if (params.multipartData) {
|
} else if (params.multipartData) {
|
||||||
const formData = new MultipartFormData();
|
const formData = new MultipartFormData();
|
||||||
|
|
@ -671,11 +675,28 @@ function serializePostData(params: channels.APIRequestContextFetchParams, header
|
||||||
else if (field.value)
|
else if (field.value)
|
||||||
formData.addField(field.name, field.value);
|
formData.addField(field.name, field.value);
|
||||||
}
|
}
|
||||||
headers['content-type'] ??= formData.contentTypeHeader();
|
setHeader(headers, 'content-type', formData.contentTypeHeader(), true);
|
||||||
return formData.finish();
|
return formData.finish();
|
||||||
} else if (params.postData !== undefined) {
|
} else if (params.postData !== undefined) {
|
||||||
headers['content-type'] ??= 'application/octet-stream';
|
setHeader(headers, 'content-type', 'application/octet-stream', true);
|
||||||
return params.postData;
|
return params.postData;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setHeader(headers: { [name: string]: string }, name: string, value: string, keepExisting = false) {
|
||||||
|
const existing = Object.entries(headers).find(pair => pair[0].toLowerCase() === name.toLowerCase());
|
||||||
|
if (!existing)
|
||||||
|
headers[name] = value;
|
||||||
|
else if (!keepExisting)
|
||||||
|
headers[existing[0]] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHeader(headers: HeadersObject, name: string) {
|
||||||
|
const existing = Object.entries(headers).find(pair => pair[0].toLowerCase() === name.toLowerCase());
|
||||||
|
return existing ? existing[1] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeHeader(headers: { [name: string]: string }, name: string) {
|
||||||
|
delete headers[name];
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -400,4 +400,20 @@ it('should throw an error when maxRedirects is less than 0', async ({ playwright
|
||||||
for (const method of ['GET', 'PUT', 'POST', 'OPTIONS', 'HEAD', 'PATCH'])
|
for (const method of ['GET', 'PUT', 'POST', 'OPTIONS', 'HEAD', 'PATCH'])
|
||||||
await expect(async () => request.fetch(`${server.PREFIX}/a/redirect1`, { method, maxRedirects: -1 })).rejects.toThrow(`'maxRedirects' should be greater than or equal to '0'`);
|
await expect(async () => request.fetch(`${server.PREFIX}/a/redirect1`, { method, maxRedirects: -1 })).rejects.toThrow(`'maxRedirects' should be greater than or equal to '0'`);
|
||||||
await request.dispose();
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should keep headers capitalization', async ({ playwright, server }) => {
|
||||||
|
const request = await playwright.request.newContext();
|
||||||
|
const [serverRequest, response] = await Promise.all([
|
||||||
|
server.waitForRequest('/empty.html'),
|
||||||
|
request.get(server.EMPTY_PAGE, {
|
||||||
|
headers: {
|
||||||
|
'X-fOo': 'vaLUE',
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
expect(serverRequest.rawHeaders).toContain('X-fOo');
|
||||||
|
expect(serverRequest.rawHeaders).toContain('vaLUE');
|
||||||
|
await request.dispose();
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue