fix(fetch): use data, form and multipart for different post data (#9248)
This commit is contained in:
parent
f3648a66a3
commit
235eaca34a
|
|
@ -38,13 +38,11 @@ If set changes the fetch method (e.g. PUT or POST). If not specified, GET method
|
||||||
|
|
||||||
Allows to set HTTP headers.
|
Allows to set HTTP headers.
|
||||||
|
|
||||||
### option: FetchRequest.fetch.data
|
### option: FetchRequest.fetch.data = %%-fetch-option-data-%%
|
||||||
- `data` <[string]|[Buffer]|[Serializable]>
|
|
||||||
|
|
||||||
Allows to set post data of the fetch. If the data parameter is an object, it will be serialized the following way:
|
### option: FetchRequest.fetch.form = %%-fetch-option-form-%%
|
||||||
* If `content-type` header is set to `application/x-www-form-urlencoded` the object will be serialized as html form using `application/x-www-form-urlencoded` encoding.
|
|
||||||
* If `content-type` header is set to `multipart/form-data` the object will be serialized as html form using `multipart/form-data` encoding.
|
### option: FetchRequest.fetch.multipart = %%-fetch-option-multipart-%%
|
||||||
* Otherwise the object will be serialized to json string and `content-type` header will be set to `application/json`.
|
|
||||||
|
|
||||||
### option: FetchRequest.fetch.timeout
|
### option: FetchRequest.fetch.timeout
|
||||||
- `timeout` <[float]>
|
- `timeout` <[float]>
|
||||||
|
|
@ -114,13 +112,11 @@ Query parameters to be send with the URL.
|
||||||
|
|
||||||
Allows to set HTTP headers.
|
Allows to set HTTP headers.
|
||||||
|
|
||||||
### option: FetchRequest.post.data
|
### option: FetchRequest.post.data = %%-fetch-option-data-%%
|
||||||
- `data` <[string]|[Buffer]|[Serializable]>
|
|
||||||
|
|
||||||
Allows to set post data of the fetch. If the data parameter is an object, it will be serialized the following way:
|
### option: FetchRequest.post.form = %%-fetch-option-form-%%
|
||||||
* If `content-type` header is set to `application/x-www-form-urlencoded` the object will be serialized as html form using `application/x-www-form-urlencoded` encoding.
|
|
||||||
* If `content-type` header is set to `multipart/form-data` the object will be serialized as html form using `multipart/form-data` encoding.
|
### option: FetchRequest.post.multipart = %%-fetch-option-multipart-%%
|
||||||
* Otherwise the object will be serialized to json string and `content-type` header will be set to `application/json`.
|
|
||||||
|
|
||||||
### option: FetchRequest.post.timeout
|
### option: FetchRequest.post.timeout
|
||||||
- `timeout` <[float]>
|
- `timeout` <[float]>
|
||||||
|
|
|
||||||
|
|
@ -298,6 +298,32 @@ Emulates consistent viewport for each page. Defaults to an 1280x720 viewport. Us
|
||||||
Emulates consistent window screen size available inside web page via `window.screen`. Is only used when the
|
Emulates consistent window screen size available inside web page via `window.screen`. Is only used when the
|
||||||
[`option: viewport`] is set.
|
[`option: viewport`] is set.
|
||||||
|
|
||||||
|
## fetch-option-form
|
||||||
|
- `form` <[Object]<[string], [string]|[float]|[boolean]>>
|
||||||
|
|
||||||
|
Provides an object that will be serialized as html form using `application/x-www-form-urlencoded` encoding and sent as
|
||||||
|
this request body. If this parameter is specified `content-type` header will be set to `application/x-www-form-urlencoded`
|
||||||
|
unless explicitly provided.
|
||||||
|
|
||||||
|
## fetch-option-multipart
|
||||||
|
- `multipart` <[Object]<[string], [string]|[float]|[boolean]|[ReadStream]|[Object]>>
|
||||||
|
- `name` <[string]> File name
|
||||||
|
- `mimeType` <[string]> File type
|
||||||
|
- `buffer` <[Buffer]> File content
|
||||||
|
|
||||||
|
Provides an object that will be serialized as html form using `multipart/form-data` encoding and sent as
|
||||||
|
this request body. If this parameter is specified `content-type` header will be set to `multipart/form-data`
|
||||||
|
unless explicitly provided. File values can be passed either as [`fs.ReadStream`](https://nodejs.org/api/fs.html#fs_class_fs_readstream)
|
||||||
|
or as file-like object containing file name, mime-type and its content.
|
||||||
|
|
||||||
|
## fetch-option-data
|
||||||
|
- `data` <[string]|[Buffer]|[Serializable]>
|
||||||
|
|
||||||
|
Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string
|
||||||
|
and `content-type` header will be set to `application/json` if not explicitly set. Otherwise the `content-type` header will be
|
||||||
|
set to `application/octet-stream` if not explicitly set.
|
||||||
|
|
||||||
|
|
||||||
## evaluate-expression
|
## evaluate-expression
|
||||||
- `expression` <[string]>
|
- `expression` <[string]>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ export type FetchOptions = {
|
||||||
method?: string,
|
method?: string,
|
||||||
headers?: Headers,
|
headers?: Headers,
|
||||||
data?: string | Buffer | Serializable,
|
data?: string | Buffer | Serializable,
|
||||||
|
form?: { [key: string]: string|number|boolean; };
|
||||||
|
multipart?: { [key: string]: string|number|boolean|fs.ReadStream|FilePayload; };
|
||||||
timeout?: number,
|
timeout?: number,
|
||||||
failOnStatusCode?: boolean,
|
failOnStatusCode?: boolean,
|
||||||
ignoreHTTPSErrors?: boolean,
|
ignoreHTTPSErrors?: boolean,
|
||||||
|
|
@ -68,16 +70,7 @@ export class FetchRequest extends ChannelOwner<channels.FetchRequestChannel, cha
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async post(
|
async post(urlOrRequest: string | api.Request, options?: Omit<FetchOptions, 'method'>): Promise<FetchResponse> {
|
||||||
urlOrRequest: string | api.Request,
|
|
||||||
options?: {
|
|
||||||
params?: { [key: string]: string; };
|
|
||||||
headers?: { [key: string]: string; };
|
|
||||||
data?: string | Buffer | Serializable;
|
|
||||||
timeout?: number;
|
|
||||||
failOnStatusCode?: boolean;
|
|
||||||
ignoreHTTPSErrors?: boolean,
|
|
||||||
}): Promise<FetchResponse> {
|
|
||||||
return this.fetch(urlOrRequest, {
|
return this.fetch(urlOrRequest, {
|
||||||
...options,
|
...options,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -88,40 +81,46 @@ export class FetchRequest extends ChannelOwner<channels.FetchRequestChannel, cha
|
||||||
return this._wrapApiCall(async (channel: channels.FetchRequestChannel) => {
|
return this._wrapApiCall(async (channel: channels.FetchRequestChannel) => {
|
||||||
const request: network.Request | undefined = (urlOrRequest instanceof network.Request) ? urlOrRequest as network.Request : undefined;
|
const request: network.Request | undefined = (urlOrRequest instanceof network.Request) ? urlOrRequest as network.Request : undefined;
|
||||||
assert(request || typeof urlOrRequest === 'string', 'First argument must be either URL string or Request');
|
assert(request || typeof urlOrRequest === 'string', 'First argument must be either URL string or Request');
|
||||||
|
assert((options.data === undefined ? 0 : 1) + (options.form === undefined ? 0 : 1) + (options.multipart === undefined ? 0 : 1) <= 1, `Only one of 'data', 'form' or 'multipart' can be specified`);
|
||||||
const url = request ? request.url() : urlOrRequest as string;
|
const url = request ? request.url() : urlOrRequest as string;
|
||||||
const params = objectToArray(options.params);
|
const params = objectToArray(options.params);
|
||||||
const method = options.method || request?.method();
|
const method = options.method || request?.method();
|
||||||
// Cannot call allHeaders() here as the request may be paused inside route handler.
|
// Cannot call allHeaders() here as the request may be paused inside route handler.
|
||||||
const headersObj = options.headers || request?.headers() ;
|
const headersObj = options.headers || request?.headers() ;
|
||||||
const headers = headersObj ? headersObjectToArray(headersObj) : undefined;
|
const headers = headersObj ? headersObjectToArray(headersObj) : undefined;
|
||||||
let formData: any;
|
let jsonData: any;
|
||||||
|
let formData: channels.NameValue[] | undefined;
|
||||||
|
let multipartData: channels.FormField[] | undefined;
|
||||||
let postDataBuffer: Buffer | undefined;
|
let postDataBuffer: Buffer | undefined;
|
||||||
if (options.data) {
|
if (options.data !== undefined) {
|
||||||
if (isString(options.data)) {
|
if (isString(options.data))
|
||||||
postDataBuffer = Buffer.from(options.data, 'utf8');
|
postDataBuffer = Buffer.from(options.data, 'utf8');
|
||||||
} else if (Buffer.isBuffer(options.data)) {
|
else if (Buffer.isBuffer(options.data))
|
||||||
postDataBuffer = options.data;
|
postDataBuffer = options.data;
|
||||||
} else if (typeof options.data === 'object') {
|
else if (typeof options.data === 'object')
|
||||||
formData = {};
|
jsonData = options.data;
|
||||||
// Convert file-like values to ServerFilePayload structs.
|
else
|
||||||
for (const [name, value] of Object.entries(options.data)) {
|
|
||||||
if (isFilePayload(value)) {
|
|
||||||
const payload = value as FilePayload;
|
|
||||||
if (!Buffer.isBuffer(payload.buffer))
|
|
||||||
throw new Error(`Unexpected buffer type of 'data.${name}'`);
|
|
||||||
formData[name] = filePayloadToJson(payload);
|
|
||||||
} else if (value instanceof fs.ReadStream) {
|
|
||||||
formData[name] = await readStreamToJson(value as fs.ReadStream);
|
|
||||||
} else {
|
|
||||||
formData[name] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error(`Unexpected 'data' type`);
|
throw new Error(`Unexpected 'data' type`);
|
||||||
|
} else if (options.form) {
|
||||||
|
formData = objectToArray(options.form);
|
||||||
|
} else if (options.multipart) {
|
||||||
|
multipartData = [];
|
||||||
|
// Convert file-like values to ServerFilePayload structs.
|
||||||
|
for (const [name, value] of Object.entries(options.multipart)) {
|
||||||
|
if (isFilePayload(value)) {
|
||||||
|
const payload = value as FilePayload;
|
||||||
|
if (!Buffer.isBuffer(payload.buffer))
|
||||||
|
throw new Error(`Unexpected buffer type of 'data.${name}'`);
|
||||||
|
multipartData.push({ name, file: filePayloadToJson(payload) });
|
||||||
|
} else if (value instanceof fs.ReadStream) {
|
||||||
|
multipartData.push({ name, file: await readStreamToJson(value as fs.ReadStream) });
|
||||||
|
} else {
|
||||||
|
multipartData.push({ name, value: String(value) });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (postDataBuffer === undefined && formData === undefined)
|
|
||||||
postDataBuffer = request?.postDataBuffer() || undefined;
|
|
||||||
}
|
}
|
||||||
|
if (postDataBuffer === undefined && jsonData === undefined && formData === undefined && multipartData === undefined)
|
||||||
|
postDataBuffer = request?.postDataBuffer() || undefined;
|
||||||
const postData = (postDataBuffer ? postDataBuffer.toString('base64') : undefined);
|
const postData = (postDataBuffer ? postDataBuffer.toString('base64') : undefined);
|
||||||
const result = await channel.fetch({
|
const result = await channel.fetch({
|
||||||
url,
|
url,
|
||||||
|
|
@ -129,7 +128,9 @@ export class FetchRequest extends ChannelOwner<channels.FetchRequestChannel, cha
|
||||||
method,
|
method,
|
||||||
headers,
|
headers,
|
||||||
postData,
|
postData,
|
||||||
|
jsonData,
|
||||||
formData,
|
formData,
|
||||||
|
multipartData,
|
||||||
timeout: options.timeout,
|
timeout: options.timeout,
|
||||||
failOnStatusCode: options.failOnStatusCode,
|
failOnStatusCode: options.failOnStatusCode,
|
||||||
ignoreHTTPSErrors: options.ignoreHTTPSErrors,
|
ignoreHTTPSErrors: options.ignoreHTTPSErrors,
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,12 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Request, Response, Route, WebSocket } from '../server/network';
|
|
||||||
import * as channels from '../protocol/channels';
|
import * as channels from '../protocol/channels';
|
||||||
import { Dispatcher, DispatcherScope, lookupNullableDispatcher, existingDispatcher } from './dispatcher';
|
|
||||||
import { FrameDispatcher } from './frameDispatcher';
|
|
||||||
import { CallMetadata } from '../server/instrumentation';
|
|
||||||
import { FetchRequest } from '../server/fetch';
|
import { FetchRequest } from '../server/fetch';
|
||||||
import { arrayToObject, headersArrayToObject } from '../utils/utils';
|
import { CallMetadata } from '../server/instrumentation';
|
||||||
|
import { Request, Response, Route, WebSocket } from '../server/network';
|
||||||
|
import { Dispatcher, DispatcherScope, existingDispatcher, lookupNullableDispatcher } from './dispatcher';
|
||||||
|
import { FrameDispatcher } from './frameDispatcher';
|
||||||
|
|
||||||
export class RequestDispatcher extends Dispatcher<Request, channels.RequestInitializer, channels.RequestEvents> implements channels.RequestChannel {
|
export class RequestDispatcher extends Dispatcher<Request, channels.RequestInitializer, channels.RequestEvents> implements channels.RequestChannel {
|
||||||
|
|
||||||
|
|
@ -186,17 +185,7 @@ export class FetchRequestDispatcher extends Dispatcher<FetchRequest, channels.Fe
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetch(params: channels.FetchRequestFetchParams, metadata?: channels.Metadata): Promise<channels.FetchRequestFetchResult> {
|
async fetch(params: channels.FetchRequestFetchParams, metadata?: channels.Metadata): Promise<channels.FetchRequestFetchResult> {
|
||||||
const { fetchResponse, error } = await this._object.fetch({
|
const { fetchResponse, error } = await this._object.fetch(params);
|
||||||
url: params.url,
|
|
||||||
params: arrayToObject(params.params),
|
|
||||||
method: params.method,
|
|
||||||
headers: params.headers ? headersArrayToObject(params.headers, false) : undefined,
|
|
||||||
postData: params.postData ? Buffer.from(params.postData, 'base64') : undefined,
|
|
||||||
formData: params.formData,
|
|
||||||
timeout: params.timeout,
|
|
||||||
failOnStatusCode: params.failOnStatusCode,
|
|
||||||
ignoreHTTPSErrors: params.ignoreHTTPSErrors,
|
|
||||||
});
|
|
||||||
let response;
|
let response;
|
||||||
if (fetchResponse) {
|
if (fetchResponse) {
|
||||||
response = {
|
response = {
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,16 @@ export type SerializedError = {
|
||||||
value?: SerializedValue,
|
value?: SerializedValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type FormField = {
|
||||||
|
name: string,
|
||||||
|
value?: string,
|
||||||
|
file?: {
|
||||||
|
name: string,
|
||||||
|
mimeType: string,
|
||||||
|
buffer: Binary,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export type InterceptedResponse = {
|
export type InterceptedResponse = {
|
||||||
request: RequestChannel,
|
request: RequestChannel,
|
||||||
status: number,
|
status: number,
|
||||||
|
|
@ -172,7 +182,9 @@ export type FetchRequestFetchParams = {
|
||||||
method?: string,
|
method?: string,
|
||||||
headers?: NameValue[],
|
headers?: NameValue[],
|
||||||
postData?: Binary,
|
postData?: Binary,
|
||||||
formData?: any,
|
jsonData?: any,
|
||||||
|
formData?: NameValue[],
|
||||||
|
multipartData?: FormField[],
|
||||||
timeout?: number,
|
timeout?: number,
|
||||||
failOnStatusCode?: boolean,
|
failOnStatusCode?: boolean,
|
||||||
ignoreHTTPSErrors?: boolean,
|
ignoreHTTPSErrors?: boolean,
|
||||||
|
|
@ -182,7 +194,9 @@ export type FetchRequestFetchOptions = {
|
||||||
method?: string,
|
method?: string,
|
||||||
headers?: NameValue[],
|
headers?: NameValue[],
|
||||||
postData?: Binary,
|
postData?: Binary,
|
||||||
formData?: any,
|
jsonData?: any,
|
||||||
|
formData?: NameValue[],
|
||||||
|
multipartData?: FormField[],
|
||||||
timeout?: number,
|
timeout?: number,
|
||||||
failOnStatusCode?: boolean,
|
failOnStatusCode?: boolean,
|
||||||
ignoreHTTPSErrors?: boolean,
|
ignoreHTTPSErrors?: boolean,
|
||||||
|
|
|
||||||
|
|
@ -214,6 +214,17 @@ SerializedError:
|
||||||
stack: string?
|
stack: string?
|
||||||
value: SerializedValue?
|
value: SerializedValue?
|
||||||
|
|
||||||
|
FormField:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name: string
|
||||||
|
value: string?
|
||||||
|
file:
|
||||||
|
type: object?
|
||||||
|
properties:
|
||||||
|
name: string
|
||||||
|
mimeType: string
|
||||||
|
buffer: binary
|
||||||
|
|
||||||
InterceptedResponse:
|
InterceptedResponse:
|
||||||
type: object
|
type: object
|
||||||
|
|
@ -242,7 +253,13 @@ FetchRequest:
|
||||||
type: array?
|
type: array?
|
||||||
items: NameValue
|
items: NameValue
|
||||||
postData: binary?
|
postData: binary?
|
||||||
formData: json?
|
jsonData: json?
|
||||||
|
formData:
|
||||||
|
type: array?
|
||||||
|
items: NameValue
|
||||||
|
multipartData:
|
||||||
|
type: array?
|
||||||
|
items: FormField
|
||||||
timeout: number?
|
timeout: number?
|
||||||
failOnStatusCode: boolean?
|
failOnStatusCode: boolean?
|
||||||
ignoreHTTPSErrors: boolean?
|
ignoreHTTPSErrors: boolean?
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,15 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||||
})),
|
})),
|
||||||
value: tOptional(tType('SerializedValue')),
|
value: tOptional(tType('SerializedValue')),
|
||||||
});
|
});
|
||||||
|
scheme.FormField = tObject({
|
||||||
|
name: tString,
|
||||||
|
value: tOptional(tString),
|
||||||
|
file: tOptional(tObject({
|
||||||
|
name: tString,
|
||||||
|
mimeType: tString,
|
||||||
|
buffer: tBinary,
|
||||||
|
})),
|
||||||
|
});
|
||||||
scheme.InterceptedResponse = tObject({
|
scheme.InterceptedResponse = tObject({
|
||||||
request: tChannel('Request'),
|
request: tChannel('Request'),
|
||||||
status: tNumber,
|
status: tNumber,
|
||||||
|
|
@ -159,7 +168,9 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||||
method: tOptional(tString),
|
method: tOptional(tString),
|
||||||
headers: tOptional(tArray(tType('NameValue'))),
|
headers: tOptional(tArray(tType('NameValue'))),
|
||||||
postData: tOptional(tBinary),
|
postData: tOptional(tBinary),
|
||||||
formData: tOptional(tAny),
|
jsonData: tOptional(tAny),
|
||||||
|
formData: tOptional(tArray(tType('NameValue'))),
|
||||||
|
multipartData: tOptional(tArray(tType('FormField'))),
|
||||||
timeout: tOptional(tNumber),
|
timeout: tOptional(tNumber),
|
||||||
failOnStatusCode: tOptional(tBoolean),
|
failOnStatusCode: tOptional(tBoolean),
|
||||||
ignoreHTTPSErrors: tOptional(tBoolean),
|
ignoreHTTPSErrors: tOptional(tBoolean),
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import zlib from 'zlib';
|
||||||
import { HTTPCredentials } from '../../types/types';
|
import { HTTPCredentials } from '../../types/types';
|
||||||
import * as channels from '../protocol/channels';
|
import * as channels from '../protocol/channels';
|
||||||
import { TimeoutSettings } from '../utils/timeoutSettings';
|
import { TimeoutSettings } from '../utils/timeoutSettings';
|
||||||
import { assert, createGuid, getPlaywrightVersion, isFilePayload, monotonicTime } from '../utils/utils';
|
import { assert, createGuid, getPlaywrightVersion, monotonicTime } from '../utils/utils';
|
||||||
import { BrowserContext } from './browserContext';
|
import { BrowserContext } from './browserContext';
|
||||||
import { CookieStore, domainMatches } from './cookieStore';
|
import { CookieStore, domainMatches } from './cookieStore';
|
||||||
import { MultipartFormData } from './formData';
|
import { MultipartFormData } from './formData';
|
||||||
|
|
@ -32,8 +32,7 @@ import { Playwright } from './playwright';
|
||||||
import * as types from './types';
|
import * as types from './types';
|
||||||
import { HeadersArray, ProxySettings } from './types';
|
import { HeadersArray, ProxySettings } from './types';
|
||||||
|
|
||||||
|
type FetchRequestOptions = {
|
||||||
export type FetchRequestOptions = {
|
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
extraHTTPHeaders?: HeadersArray;
|
extraHTTPHeaders?: HeadersArray;
|
||||||
httpCredentials?: HTTPCredentials;
|
httpCredentials?: HTTPCredentials;
|
||||||
|
|
@ -84,7 +83,7 @@ export abstract class FetchRequest extends SdkObject {
|
||||||
return uid;
|
return uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetch(params: types.FetchOptions): Promise<{fetchResponse?: Omit<types.FetchResponse, 'body'> & { fetchUid: string }, error?: string}> {
|
async fetch(params: channels.FetchRequestFetchParams): Promise<{fetchResponse?: Omit<types.FetchResponse, 'body'> & { fetchUid: string }, error?: string}> {
|
||||||
try {
|
try {
|
||||||
const headers: { [name: string]: string } = {};
|
const headers: { [name: string]: string } = {};
|
||||||
const defaults = this._defaultOptions();
|
const defaults = this._defaultOptions();
|
||||||
|
|
@ -98,7 +97,7 @@ export abstract class FetchRequest extends SdkObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.headers) {
|
if (params.headers) {
|
||||||
for (const [name, value] of Object.entries(params.headers))
|
for (const { name, value } of params.headers)
|
||||||
headers[name.toLowerCase()] = value;
|
headers[name.toLowerCase()] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,19 +129,17 @@ export abstract class FetchRequest extends SdkObject {
|
||||||
|
|
||||||
const requestUrl = new URL(params.url, defaults.baseURL);
|
const requestUrl = new URL(params.url, defaults.baseURL);
|
||||||
if (params.params) {
|
if (params.params) {
|
||||||
for (const [name, value] of Object.entries(params.params))
|
for (const { name, value } of params.params)
|
||||||
requestUrl.searchParams.set(name, value);
|
requestUrl.searchParams.set(name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
let postData;
|
let postData;
|
||||||
if (['POST', 'PUSH', 'PATCH'].includes(method))
|
if (['POST', 'PUSH', 'PATCH'].includes(method))
|
||||||
postData = params.formData ? serializeFormData(params.formData, headers) : params.postData;
|
postData = serializePostData(params, headers);
|
||||||
else if (params.postData || params.formData)
|
else if (params.postData || params.jsonData || params.formData || params.multipartData)
|
||||||
throw new Error(`Method ${method} does not accept post data`);
|
throw new Error(`Method ${method} does not accept post data`);
|
||||||
if (postData) {
|
if (postData)
|
||||||
headers['content-length'] = String(postData.byteLength);
|
headers['content-length'] = String(postData.byteLength);
|
||||||
headers['content-type'] ??= 'application/octet-stream';
|
|
||||||
}
|
|
||||||
const fetchResponse = await this._sendRequest(requestUrl, options, postData);
|
const fetchResponse = await this._sendRequest(requestUrl, options, postData);
|
||||||
const fetchUid = this._storeResponseBody(fetchResponse.body);
|
const fetchUid = this._storeResponseBody(fetchResponse.body);
|
||||||
if (params.failOnStatusCode && (fetchResponse.status < 200 || fetchResponse.status >= 400))
|
if (params.failOnStatusCode && (fetchResponse.status < 200 || fetchResponse.status >= 400))
|
||||||
|
|
@ -458,30 +455,31 @@ function parseCookie(header: string): types.NetworkCookie | null {
|
||||||
return cookie;
|
return cookie;
|
||||||
}
|
}
|
||||||
|
|
||||||
function serializeFormData(data: any, headers: { [name: string]: string }): Buffer {
|
function serializePostData(params: channels.FetchRequestFetchParams, headers: { [name: string]: string }): Buffer | undefined {
|
||||||
const contentType = headers['content-type'] || 'application/json';
|
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 (contentType === 'application/json') {
|
if (params.jsonData) {
|
||||||
const json = JSON.stringify(data);
|
const json = JSON.stringify(params.jsonData);
|
||||||
headers['content-type'] ??= contentType;
|
headers['content-type'] ??= 'application/json';
|
||||||
return Buffer.from(json, 'utf8');
|
return Buffer.from(json, 'utf8');
|
||||||
} else if (contentType === 'application/x-www-form-urlencoded') {
|
} else if (params.formData) {
|
||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams();
|
||||||
for (const [name, value] of Object.entries(data))
|
for (const { name, value } of params.formData)
|
||||||
searchParams.append(name, String(value));
|
searchParams.append(name, value);
|
||||||
|
headers['content-type'] ??= 'application/x-www-form-urlencoded';
|
||||||
return Buffer.from(searchParams.toString(), 'utf8');
|
return Buffer.from(searchParams.toString(), 'utf8');
|
||||||
} else if (contentType === 'multipart/form-data') {
|
} else if (params.multipartData) {
|
||||||
const formData = new MultipartFormData();
|
const formData = new MultipartFormData();
|
||||||
for (const [name, value] of Object.entries(data)) {
|
for (const field of params.multipartData) {
|
||||||
if (isFilePayload(value)) {
|
if (field.file)
|
||||||
const payload = value as types.FilePayload;
|
formData.addFileField(field.name, field.file);
|
||||||
formData.addFileField(name, payload);
|
else if (field.value)
|
||||||
} else if (value !== undefined) {
|
formData.addField(field.name, field.value);
|
||||||
formData.addField(name, String(value));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
headers['content-type'] = formData.contentTypeHeader();
|
headers['content-type'] ??= formData.contentTypeHeader();
|
||||||
return formData.finish();
|
return formData.finish();
|
||||||
} else {
|
} else if (params.postData) {
|
||||||
throw new Error(`Cannot serialize data using content type: ${contentType}`);
|
headers['content-type'] ??= 'application/octet-stream';
|
||||||
|
return Buffer.from(params.postData, 'base64');
|
||||||
}
|
}
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -372,30 +372,6 @@ export type SetStorageState = {
|
||||||
origins?: OriginStorage[]
|
origins?: OriginStorage[]
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FileInfo = {
|
|
||||||
name: string,
|
|
||||||
mimeType?: string,
|
|
||||||
buffer: Buffer,
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FormField = {
|
|
||||||
name: string,
|
|
||||||
value?: string,
|
|
||||||
file?: FileInfo,
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FetchOptions = {
|
|
||||||
url: string,
|
|
||||||
params?: { [name: string]: string },
|
|
||||||
method?: string,
|
|
||||||
headers?: { [name: string]: string },
|
|
||||||
postData?: Buffer,
|
|
||||||
formData?: FormField[],
|
|
||||||
timeout?: number,
|
|
||||||
failOnStatusCode?: boolean,
|
|
||||||
ignoreHTTPSErrors?: boolean,
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FetchResponse = {
|
export type FetchResponse = {
|
||||||
url: string,
|
url: string,
|
||||||
status: number,
|
status: number,
|
||||||
|
|
|
||||||
|
|
@ -384,12 +384,12 @@ class HashStream extends stream.Writable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function objectToArray(map?: { [key: string]: string }): NameValue[] | undefined {
|
export function objectToArray(map?: { [key: string]: any }): NameValue[] | undefined {
|
||||||
if (!map)
|
if (!map)
|
||||||
return undefined;
|
return undefined;
|
||||||
const result = [];
|
const result = [];
|
||||||
for (const [name, value] of Object.entries(map))
|
for (const [name, value] of Object.entries(map))
|
||||||
result.push({ name, value });
|
result.push({ name, value: String(value) });
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -726,10 +726,7 @@ it('should support application/x-www-form-urlencoded', async function({ context,
|
||||||
const [req] = await Promise.all([
|
const [req] = await Promise.all([
|
||||||
server.waitForRequest('/empty.html'),
|
server.waitForRequest('/empty.html'),
|
||||||
context._request.post(server.EMPTY_PAGE, {
|
context._request.post(server.EMPTY_PAGE, {
|
||||||
headers: {
|
form: {
|
||||||
'content-type': 'application/x-www-form-urlencoded'
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
firstName: 'John',
|
firstName: 'John',
|
||||||
lastName: 'Doe',
|
lastName: 'Doe',
|
||||||
file: 'f.js',
|
file: 'f.js',
|
||||||
|
|
@ -784,10 +781,7 @@ it('should support multipart/form-data', async function({ context, page, server
|
||||||
const [{ error, fields, files, serverRequest }, response] = await Promise.all([
|
const [{ error, fields, files, serverRequest }, response] = await Promise.all([
|
||||||
formReceived,
|
formReceived,
|
||||||
context._request.post(server.EMPTY_PAGE, {
|
context._request.post(server.EMPTY_PAGE, {
|
||||||
headers: {
|
multipart: {
|
||||||
'content-type': 'multipart/form-data'
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
firstName: 'John',
|
firstName: 'John',
|
||||||
lastName: 'Doe',
|
lastName: 'Doe',
|
||||||
file
|
file
|
||||||
|
|
@ -819,10 +813,7 @@ it('should support multipart/form-data with ReadSream values', async function({
|
||||||
const [{ error, fields, files, serverRequest }, response] = await Promise.all([
|
const [{ error, fields, files, serverRequest }, response] = await Promise.all([
|
||||||
formReceived,
|
formReceived,
|
||||||
context._request.post(server.EMPTY_PAGE, {
|
context._request.post(server.EMPTY_PAGE, {
|
||||||
headers: {
|
multipart: {
|
||||||
'content-type': 'multipart/form-data'
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
firstName: 'John',
|
firstName: 'John',
|
||||||
lastName: 'Doe',
|
lastName: 'Doe',
|
||||||
readStream
|
readStream
|
||||||
|
|
@ -841,17 +832,24 @@ it('should support multipart/form-data with ReadSream values', async function({
|
||||||
expect(response.status()).toBe(200);
|
expect(response.status()).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw nice error on unsupported encoding', async function({ context, server }) {
|
it('should serialize data to json regardless of content-type', async function({ context, server }) {
|
||||||
const error = await context._request.post(server.EMPTY_PAGE, {
|
const data = {
|
||||||
headers: {
|
firstName: 'John',
|
||||||
'content-type': 'unknown'
|
lastName: 'Doe',
|
||||||
},
|
};
|
||||||
data: {
|
const [req] = await Promise.all([
|
||||||
firstName: 'John',
|
server.waitForRequest('/empty.html'),
|
||||||
lastName: 'Doe',
|
context._request.post(server.EMPTY_PAGE, {
|
||||||
}
|
headers: {
|
||||||
}).catch(e => e);
|
'content-type': 'unknown'
|
||||||
expect(error.message).toContain('Cannot serialize data using content type: unknown');
|
},
|
||||||
|
data
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
expect(req.method).toBe('POST');
|
||||||
|
expect(req.headers['content-type']).toBe('unknown');
|
||||||
|
const body = (await req.postBody).toString('utf8');
|
||||||
|
expect(body).toEqual(JSON.stringify(data));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw nice error on unsupported data type', async function({ context, server }) {
|
it('should throw nice error on unsupported data type', async function({ context, server }) {
|
||||||
|
|
@ -867,9 +865,6 @@ it('should throw nice error on unsupported data type', async function({ context,
|
||||||
it('should throw when data passed for unsupported request', async function({ context, server }) {
|
it('should throw when data passed for unsupported request', async function({ context, server }) {
|
||||||
const error = await context._request.fetch(server.EMPTY_PAGE, {
|
const error = await context._request.fetch(server.EMPTY_PAGE, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
|
||||||
'content-type': 'application/json'
|
|
||||||
},
|
|
||||||
data: {
|
data: {
|
||||||
foo: 'bar'
|
foo: 'bar'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
79
types/types.d.ts
vendored
79
types/types.d.ts
vendored
|
|
@ -18,6 +18,7 @@ import { Protocol } from './protocol';
|
||||||
import { ChildProcess } from 'child_process';
|
import { ChildProcess } from 'child_process';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
|
import { ReadStream } from 'fs';
|
||||||
import { Serializable, EvaluationArgument, PageFunction, PageFunctionOn, SmartHandle, ElementHandleForTag, BindingSource } from './structs';
|
import { Serializable, EvaluationArgument, PageFunction, PageFunctionOn, SmartHandle, ElementHandleForTag, BindingSource } from './structs';
|
||||||
|
|
||||||
type PageWaitForSelectorOptionsNotHidden = PageWaitForSelectorOptions & {
|
type PageWaitForSelectorOptionsNotHidden = PageWaitForSelectorOptions & {
|
||||||
|
|
@ -12682,12 +12683,9 @@ export interface FetchRequest {
|
||||||
*/
|
*/
|
||||||
fetch(urlOrRequest: string|Request, options?: {
|
fetch(urlOrRequest: string|Request, options?: {
|
||||||
/**
|
/**
|
||||||
* Allows to set post data of the fetch. If the data parameter is an object, it will be serialized the following way:
|
* Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and
|
||||||
* - If `content-type` header is set to `application/x-www-form-urlencoded` the object will be serialized as html form
|
* `content-type` header will be set to `application/json` if not explicitly set. Otherwise the `content-type` header will
|
||||||
* using `application/x-www-form-urlencoded` encoding.
|
* be set to `application/octet-stream` if not explicitly set.
|
||||||
* - If `content-type` header is set to `multipart/form-data` the object will be serialized as html form using
|
|
||||||
* `multipart/form-data` encoding.
|
|
||||||
* - Otherwise the object will be serialized to json string and `content-type` header will be set to `application/json`.
|
|
||||||
*/
|
*/
|
||||||
data?: string|Buffer|Serializable;
|
data?: string|Buffer|Serializable;
|
||||||
|
|
||||||
|
|
@ -12696,6 +12694,13 @@ export interface FetchRequest {
|
||||||
*/
|
*/
|
||||||
failOnStatusCode?: boolean;
|
failOnStatusCode?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides an object that will be serialized as html form using `application/x-www-form-urlencoded` encoding and sent as
|
||||||
|
* this request body. If this parameter is specified `content-type` header will be set to
|
||||||
|
* `application/x-www-form-urlencoded` unless explicitly provided.
|
||||||
|
*/
|
||||||
|
form?: { [key: string]: string|number|boolean; };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows to set HTTP headers.
|
* Allows to set HTTP headers.
|
||||||
*/
|
*/
|
||||||
|
|
@ -12711,6 +12716,29 @@ export interface FetchRequest {
|
||||||
*/
|
*/
|
||||||
method?: string;
|
method?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides an object that will be serialized as html form using `multipart/form-data` encoding and sent as this request
|
||||||
|
* body. If this parameter is specified `content-type` header will be set to `multipart/form-data` unless explicitly
|
||||||
|
* provided. File values can be passed either as [`fs.ReadStream`](https://nodejs.org/api/fs.html#fs_class_fs_readstream)
|
||||||
|
* or as file-like object containing file name, mime-type and its content.
|
||||||
|
*/
|
||||||
|
multipart?: { [key: string]: string|number|boolean|ReadStream|{
|
||||||
|
/**
|
||||||
|
* File name
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File type
|
||||||
|
*/
|
||||||
|
mimeType: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File content
|
||||||
|
*/
|
||||||
|
buffer: Buffer;
|
||||||
|
}; };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query parameters to be send with the URL.
|
* Query parameters to be send with the URL.
|
||||||
*/
|
*/
|
||||||
|
|
@ -12763,12 +12791,9 @@ export interface FetchRequest {
|
||||||
*/
|
*/
|
||||||
post(urlOrRequest: string|Request, options?: {
|
post(urlOrRequest: string|Request, options?: {
|
||||||
/**
|
/**
|
||||||
* Allows to set post data of the fetch. If the data parameter is an object, it will be serialized the following way:
|
* Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and
|
||||||
* - If `content-type` header is set to `application/x-www-form-urlencoded` the object will be serialized as html form
|
* `content-type` header will be set to `application/json` if not explicitly set. Otherwise the `content-type` header will
|
||||||
* using `application/x-www-form-urlencoded` encoding.
|
* be set to `application/octet-stream` if not explicitly set.
|
||||||
* - If `content-type` header is set to `multipart/form-data` the object will be serialized as html form using
|
|
||||||
* `multipart/form-data` encoding.
|
|
||||||
* - Otherwise the object will be serialized to json string and `content-type` header will be set to `application/json`.
|
|
||||||
*/
|
*/
|
||||||
data?: string|Buffer|Serializable;
|
data?: string|Buffer|Serializable;
|
||||||
|
|
||||||
|
|
@ -12777,6 +12802,13 @@ export interface FetchRequest {
|
||||||
*/
|
*/
|
||||||
failOnStatusCode?: boolean;
|
failOnStatusCode?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides an object that will be serialized as html form using `application/x-www-form-urlencoded` encoding and sent as
|
||||||
|
* this request body. If this parameter is specified `content-type` header will be set to
|
||||||
|
* `application/x-www-form-urlencoded` unless explicitly provided.
|
||||||
|
*/
|
||||||
|
form?: { [key: string]: string|number|boolean; };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows to set HTTP headers.
|
* Allows to set HTTP headers.
|
||||||
*/
|
*/
|
||||||
|
|
@ -12787,6 +12819,29 @@ export interface FetchRequest {
|
||||||
*/
|
*/
|
||||||
ignoreHTTPSErrors?: boolean;
|
ignoreHTTPSErrors?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides an object that will be serialized as html form using `multipart/form-data` encoding and sent as this request
|
||||||
|
* body. If this parameter is specified `content-type` header will be set to `multipart/form-data` unless explicitly
|
||||||
|
* provided. File values can be passed either as [`fs.ReadStream`](https://nodejs.org/api/fs.html#fs_class_fs_readstream)
|
||||||
|
* or as file-like object containing file name, mime-type and its content.
|
||||||
|
*/
|
||||||
|
multipart?: { [key: string]: string|number|boolean|ReadStream|{
|
||||||
|
/**
|
||||||
|
* File name
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File type
|
||||||
|
*/
|
||||||
|
mimeType: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File content
|
||||||
|
*/
|
||||||
|
buffer: Buffer;
|
||||||
|
}; };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query parameters to be send with the URL.
|
* Query parameters to be send with the URL.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
1
utils/generate_types/overrides.d.ts
vendored
1
utils/generate_types/overrides.d.ts
vendored
|
|
@ -17,6 +17,7 @@ import { Protocol } from './protocol';
|
||||||
import { ChildProcess } from 'child_process';
|
import { ChildProcess } from 'child_process';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
|
import { ReadStream } from 'fs';
|
||||||
import { Serializable, EvaluationArgument, PageFunction, PageFunctionOn, SmartHandle, ElementHandleForTag, BindingSource } from './structs';
|
import { Serializable, EvaluationArgument, PageFunction, PageFunctionOn, SmartHandle, ElementHandleForTag, BindingSource } from './structs';
|
||||||
|
|
||||||
type PageWaitForSelectorOptionsNotHidden = PageWaitForSelectorOptions & {
|
type PageWaitForSelectorOptionsNotHidden = PageWaitForSelectorOptions & {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue