fix(fetch): use data, form and multipart for different post data (#9248)

This commit is contained in:
Yury Semikhatsky 2021-10-01 12:11:33 -07:00 committed by GitHub
parent f3648a66a3
commit 235eaca34a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 237 additions and 158 deletions

View file

@ -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]>

View file

@ -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]>

View file

@ -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,

View file

@ -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 = {

View file

@ -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,

View file

@ -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?

View file

@ -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),

View file

@ -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;
} }

View file

@ -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,

View file

@ -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;
} }

View file

@ -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
View file

@ -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.
*/ */

View file

@ -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 & {