From 235eaca34ab9e24d2a38a428ac77f0397dfafeb7 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 1 Oct 2021 12:11:33 -0700 Subject: [PATCH] fix(fetch): use data, form and multipart for different post data (#9248) --- docs/src/api/class-fetchrequest.md | 20 +++---- docs/src/api/params.md | 26 +++++++++ src/client/fetch.ts | 65 +++++++++++----------- src/dispatchers/networkDispatchers.ts | 21 ++----- src/protocol/channels.ts | 18 +++++- src/protocol/protocol.yml | 19 ++++++- src/protocol/validator.ts | 13 ++++- src/server/fetch.ts | 58 ++++++++++---------- src/server/types.ts | 24 -------- src/utils/utils.ts | 4 +- tests/browsercontext-fetch.spec.ts | 47 +++++++--------- types/types.d.ts | 79 +++++++++++++++++++++++---- utils/generate_types/overrides.d.ts | 1 + 13 files changed, 237 insertions(+), 158 deletions(-) diff --git a/docs/src/api/class-fetchrequest.md b/docs/src/api/class-fetchrequest.md index 98d6b773e5..27aad8fded 100644 --- a/docs/src/api/class-fetchrequest.md +++ b/docs/src/api/class-fetchrequest.md @@ -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. -### option: FetchRequest.fetch.data -- `data` <[string]|[Buffer]|[Serializable]> +### option: FetchRequest.fetch.data = %%-fetch-option-data-%% -Allows to set post data of the fetch. If the data parameter is an object, it will be serialized the following way: -* 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. -* Otherwise the object will be serialized to json string and `content-type` header will be set to `application/json`. +### option: FetchRequest.fetch.form = %%-fetch-option-form-%% + +### option: FetchRequest.fetch.multipart = %%-fetch-option-multipart-%% ### option: FetchRequest.fetch.timeout - `timeout` <[float]> @@ -114,13 +112,11 @@ Query parameters to be send with the URL. Allows to set HTTP headers. -### option: FetchRequest.post.data -- `data` <[string]|[Buffer]|[Serializable]> +### option: FetchRequest.post.data = %%-fetch-option-data-%% -Allows to set post data of the fetch. If the data parameter is an object, it will be serialized the following way: -* 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. -* Otherwise the object will be serialized to json string and `content-type` header will be set to `application/json`. +### option: FetchRequest.post.form = %%-fetch-option-form-%% + +### option: FetchRequest.post.multipart = %%-fetch-option-multipart-%% ### option: FetchRequest.post.timeout - `timeout` <[float]> diff --git a/docs/src/api/params.md b/docs/src/api/params.md index 37e02bea9f..47d79018f0 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -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 [`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 - `expression` <[string]> diff --git a/src/client/fetch.ts b/src/client/fetch.ts index 2186733eaa..6d3a02184a 100644 --- a/src/client/fetch.ts +++ b/src/client/fetch.ts @@ -33,6 +33,8 @@ export type FetchOptions = { method?: string, headers?: Headers, data?: string | Buffer | Serializable, + form?: { [key: string]: string|number|boolean; }; + multipart?: { [key: string]: string|number|boolean|fs.ReadStream|FilePayload; }; timeout?: number, failOnStatusCode?: boolean, ignoreHTTPSErrors?: boolean, @@ -68,16 +70,7 @@ export class FetchRequest extends ChannelOwner { + async post(urlOrRequest: string | api.Request, options?: Omit): Promise { return this.fetch(urlOrRequest, { ...options, method: 'POST', @@ -88,40 +81,46 @@ export class FetchRequest extends ChannelOwner { 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((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 params = objectToArray(options.params); const method = options.method || request?.method(); // Cannot call allHeaders() here as the request may be paused inside route handler. const headersObj = options.headers || request?.headers() ; 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; - if (options.data) { - if (isString(options.data)) { + if (options.data !== undefined) { + if (isString(options.data)) postDataBuffer = Buffer.from(options.data, 'utf8'); - } else if (Buffer.isBuffer(options.data)) { + else if (Buffer.isBuffer(options.data)) postDataBuffer = options.data; - } else if (typeof options.data === 'object') { - formData = {}; - // Convert file-like values to ServerFilePayload structs. - 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 { + else if (typeof options.data === 'object') + jsonData = options.data; + else 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 result = await channel.fetch({ url, @@ -129,7 +128,9 @@ export class FetchRequest extends ChannelOwner implements channels.RequestChannel { @@ -186,17 +185,7 @@ export class FetchRequestDispatcher extends Dispatcher { - const { fetchResponse, error } = await this._object.fetch({ - 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, - }); + const { fetchResponse, error } = await this._object.fetch(params); let response; if (fetchResponse) { response = { diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index aeb413382d..06c1765393 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -150,6 +150,16 @@ export type SerializedError = { value?: SerializedValue, }; +export type FormField = { + name: string, + value?: string, + file?: { + name: string, + mimeType: string, + buffer: Binary, + }, +}; + export type InterceptedResponse = { request: RequestChannel, status: number, @@ -172,7 +182,9 @@ export type FetchRequestFetchParams = { method?: string, headers?: NameValue[], postData?: Binary, - formData?: any, + jsonData?: any, + formData?: NameValue[], + multipartData?: FormField[], timeout?: number, failOnStatusCode?: boolean, ignoreHTTPSErrors?: boolean, @@ -182,7 +194,9 @@ export type FetchRequestFetchOptions = { method?: string, headers?: NameValue[], postData?: Binary, - formData?: any, + jsonData?: any, + formData?: NameValue[], + multipartData?: FormField[], timeout?: number, failOnStatusCode?: boolean, ignoreHTTPSErrors?: boolean, diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index 71fc21a2e2..fa5b7614cb 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -214,6 +214,17 @@ SerializedError: stack: string? value: SerializedValue? +FormField: + type: object + properties: + name: string + value: string? + file: + type: object? + properties: + name: string + mimeType: string + buffer: binary InterceptedResponse: type: object @@ -242,7 +253,13 @@ FetchRequest: type: array? items: NameValue postData: binary? - formData: json? + jsonData: json? + formData: + type: array? + items: NameValue + multipartData: + type: array? + items: FormField timeout: number? failOnStatusCode: boolean? ignoreHTTPSErrors: boolean? diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index 97bffdc8f7..8aee49aa20 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -147,6 +147,15 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { })), value: tOptional(tType('SerializedValue')), }); + scheme.FormField = tObject({ + name: tString, + value: tOptional(tString), + file: tOptional(tObject({ + name: tString, + mimeType: tString, + buffer: tBinary, + })), + }); scheme.InterceptedResponse = tObject({ request: tChannel('Request'), status: tNumber, @@ -159,7 +168,9 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { method: tOptional(tString), headers: tOptional(tArray(tType('NameValue'))), postData: tOptional(tBinary), - formData: tOptional(tAny), + jsonData: tOptional(tAny), + formData: tOptional(tArray(tType('NameValue'))), + multipartData: tOptional(tArray(tType('FormField'))), timeout: tOptional(tNumber), failOnStatusCode: tOptional(tBoolean), ignoreHTTPSErrors: tOptional(tBoolean), diff --git a/src/server/fetch.ts b/src/server/fetch.ts index e9a415eb08..b257c1e0f0 100644 --- a/src/server/fetch.ts +++ b/src/server/fetch.ts @@ -23,7 +23,7 @@ import zlib from 'zlib'; import { HTTPCredentials } from '../../types/types'; import * as channels from '../protocol/channels'; 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 { CookieStore, domainMatches } from './cookieStore'; import { MultipartFormData } from './formData'; @@ -32,8 +32,7 @@ import { Playwright } from './playwright'; import * as types from './types'; import { HeadersArray, ProxySettings } from './types'; - -export type FetchRequestOptions = { +type FetchRequestOptions = { userAgent: string; extraHTTPHeaders?: HeadersArray; httpCredentials?: HTTPCredentials; @@ -84,7 +83,7 @@ export abstract class FetchRequest extends SdkObject { return uid; } - async fetch(params: types.FetchOptions): Promise<{fetchResponse?: Omit & { fetchUid: string }, error?: string}> { + async fetch(params: channels.FetchRequestFetchParams): Promise<{fetchResponse?: Omit & { fetchUid: string }, error?: string}> { try { const headers: { [name: string]: string } = {}; const defaults = this._defaultOptions(); @@ -98,7 +97,7 @@ export abstract class FetchRequest extends SdkObject { } if (params.headers) { - for (const [name, value] of Object.entries(params.headers)) + for (const { name, value } of params.headers) headers[name.toLowerCase()] = value; } @@ -130,19 +129,17 @@ export abstract class FetchRequest extends SdkObject { const requestUrl = new URL(params.url, defaults.baseURL); if (params.params) { - for (const [name, value] of Object.entries(params.params)) + for (const { name, value } of params.params) requestUrl.searchParams.set(name, value); } let postData; if (['POST', 'PUSH', 'PATCH'].includes(method)) - postData = params.formData ? serializeFormData(params.formData, headers) : params.postData; - else if (params.postData || params.formData) + postData = serializePostData(params, headers); + else if (params.postData || params.jsonData || params.formData || params.multipartData) throw new Error(`Method ${method} does not accept post data`); - if (postData) { + if (postData) headers['content-length'] = String(postData.byteLength); - headers['content-type'] ??= 'application/octet-stream'; - } const fetchResponse = await this._sendRequest(requestUrl, options, postData); const fetchUid = this._storeResponseBody(fetchResponse.body); if (params.failOnStatusCode && (fetchResponse.status < 200 || fetchResponse.status >= 400)) @@ -458,30 +455,31 @@ function parseCookie(header: string): types.NetworkCookie | null { return cookie; } -function serializeFormData(data: any, headers: { [name: string]: string }): Buffer { - const contentType = headers['content-type'] || 'application/json'; - if (contentType === 'application/json') { - const json = JSON.stringify(data); - headers['content-type'] ??= contentType; +function serializePostData(params: channels.FetchRequestFetchParams, headers: { [name: string]: string }): 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`); + if (params.jsonData) { + const json = JSON.stringify(params.jsonData); + headers['content-type'] ??= 'application/json'; return Buffer.from(json, 'utf8'); - } else if (contentType === 'application/x-www-form-urlencoded') { + } else if (params.formData) { const searchParams = new URLSearchParams(); - for (const [name, value] of Object.entries(data)) - searchParams.append(name, String(value)); + for (const { name, value } of params.formData) + searchParams.append(name, value); + headers['content-type'] ??= 'application/x-www-form-urlencoded'; return Buffer.from(searchParams.toString(), 'utf8'); - } else if (contentType === 'multipart/form-data') { + } else if (params.multipartData) { const formData = new MultipartFormData(); - for (const [name, value] of Object.entries(data)) { - if (isFilePayload(value)) { - const payload = value as types.FilePayload; - formData.addFileField(name, payload); - } else if (value !== undefined) { - formData.addField(name, String(value)); - } + for (const field of params.multipartData) { + if (field.file) + formData.addFileField(field.name, field.file); + else if (field.value) + formData.addField(field.name, field.value); } - headers['content-type'] = formData.contentTypeHeader(); + headers['content-type'] ??= formData.contentTypeHeader(); return formData.finish(); - } else { - throw new Error(`Cannot serialize data using content type: ${contentType}`); + } else if (params.postData) { + headers['content-type'] ??= 'application/octet-stream'; + return Buffer.from(params.postData, 'base64'); } + return undefined; } diff --git a/src/server/types.ts b/src/server/types.ts index 08b7ed80fb..3f90028135 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -372,30 +372,6 @@ export type SetStorageState = { 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 = { url: string, status: number, diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 9f0030232c..80fe8b7280 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -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) return undefined; const result = []; for (const [name, value] of Object.entries(map)) - result.push({ name, value }); + result.push({ name, value: String(value) }); return result; } diff --git a/tests/browsercontext-fetch.spec.ts b/tests/browsercontext-fetch.spec.ts index 2c5bbc016d..34b86096c0 100644 --- a/tests/browsercontext-fetch.spec.ts +++ b/tests/browsercontext-fetch.spec.ts @@ -726,10 +726,7 @@ it('should support application/x-www-form-urlencoded', async function({ context, const [req] = await Promise.all([ server.waitForRequest('/empty.html'), context._request.post(server.EMPTY_PAGE, { - headers: { - 'content-type': 'application/x-www-form-urlencoded' - }, - data: { + form: { firstName: 'John', lastName: 'Doe', 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([ formReceived, context._request.post(server.EMPTY_PAGE, { - headers: { - 'content-type': 'multipart/form-data' - }, - data: { + multipart: { firstName: 'John', lastName: 'Doe', 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([ formReceived, context._request.post(server.EMPTY_PAGE, { - headers: { - 'content-type': 'multipart/form-data' - }, - data: { + multipart: { firstName: 'John', lastName: 'Doe', readStream @@ -841,17 +832,24 @@ it('should support multipart/form-data with ReadSream values', async function({ expect(response.status()).toBe(200); }); -it('should throw nice error on unsupported encoding', async function({ context, server }) { - const error = await context._request.post(server.EMPTY_PAGE, { - headers: { - 'content-type': 'unknown' - }, - data: { - firstName: 'John', - lastName: 'Doe', - } - }).catch(e => e); - expect(error.message).toContain('Cannot serialize data using content type: unknown'); +it('should serialize data to json regardless of content-type', async function({ context, server }) { + const data = { + firstName: 'John', + lastName: 'Doe', + }; + const [req] = await Promise.all([ + server.waitForRequest('/empty.html'), + context._request.post(server.EMPTY_PAGE, { + headers: { + '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 }) { @@ -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 }) { const error = await context._request.fetch(server.EMPTY_PAGE, { method: 'GET', - headers: { - 'content-type': 'application/json' - }, data: { foo: 'bar' } diff --git a/types/types.d.ts b/types/types.d.ts index 20cca1fc54..0a94d3e821 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -18,6 +18,7 @@ import { Protocol } from './protocol'; import { ChildProcess } from 'child_process'; import { EventEmitter } from 'events'; import { Readable } from 'stream'; +import { ReadStream } from 'fs'; import { Serializable, EvaluationArgument, PageFunction, PageFunctionOn, SmartHandle, ElementHandleForTag, BindingSource } from './structs'; type PageWaitForSelectorOptionsNotHidden = PageWaitForSelectorOptions & { @@ -12682,12 +12683,9 @@ export interface FetchRequest { */ 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: - * - 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. - * - Otherwise the object will be serialized to json string and `content-type` header will be set to `application/json`. + * 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. */ data?: string|Buffer|Serializable; @@ -12696,6 +12694,13 @@ export interface FetchRequest { */ 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. */ @@ -12711,6 +12716,29 @@ export interface FetchRequest { */ 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. */ @@ -12763,12 +12791,9 @@ export interface FetchRequest { */ 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: - * - 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. - * - Otherwise the object will be serialized to json string and `content-type` header will be set to `application/json`. + * 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. */ data?: string|Buffer|Serializable; @@ -12777,6 +12802,13 @@ export interface FetchRequest { */ 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. */ @@ -12787,6 +12819,29 @@ export interface FetchRequest { */ 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. */ diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index 680c0b9f8e..6489f7826a 100644 --- a/utils/generate_types/overrides.d.ts +++ b/utils/generate_types/overrides.d.ts @@ -17,6 +17,7 @@ import { Protocol } from './protocol'; import { ChildProcess } from 'child_process'; import { EventEmitter } from 'events'; import { Readable } from 'stream'; +import { ReadStream } from 'fs'; import { Serializable, EvaluationArgument, PageFunction, PageFunctionOn, SmartHandle, ElementHandleForTag, BindingSource } from './structs'; type PageWaitForSelectorOptionsNotHidden = PageWaitForSelectorOptions & {