mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(fetch): support form data and json encodings (#8975)
This commit is contained in:
parent
43213614a1
commit
806a71a4f0
@ -39,9 +39,12 @@ 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
|
||||||
- `data` <[string]|[Buffer]>
|
- `data` <[string]|[Buffer]|[Serializable]>
|
||||||
|
|
||||||
Allows to set post data of the fetch.
|
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.timeout
|
### option: FetchRequest.fetch.timeout
|
||||||
- `timeout` <[float]>
|
- `timeout` <[float]>
|
||||||
@ -108,9 +111,12 @@ 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
|
||||||
- `data` <[string]|[Buffer]>
|
- `data` <[string]|[Buffer]|[Serializable]>
|
||||||
|
|
||||||
Allows to set post data of the fetch.
|
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.timeout
|
### option: FetchRequest.post.timeout
|
||||||
- `timeout` <[float]>
|
- `timeout` <[float]>
|
||||||
|
|||||||
@ -14,21 +14,25 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { ReadStream } from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import * as mime from 'mime';
|
||||||
|
import { Serializable } from '../../types/structs';
|
||||||
import * as api from '../../types/types';
|
import * as api from '../../types/types';
|
||||||
import { HeadersArray } from '../common/types';
|
import { HeadersArray } from '../common/types';
|
||||||
import * as channels from '../protocol/channels';
|
import * as channels from '../protocol/channels';
|
||||||
import { kBrowserOrContextClosedError } from '../utils/errors';
|
import { kBrowserOrContextClosedError } from '../utils/errors';
|
||||||
import { assert, headersObjectToArray, isString, objectToArray } from '../utils/utils';
|
import { assert, headersObjectToArray, isFilePayload, isString, objectToArray } from '../utils/utils';
|
||||||
import { ChannelOwner } from './channelOwner';
|
import { ChannelOwner } from './channelOwner';
|
||||||
import * as network from './network';
|
import * as network from './network';
|
||||||
import { RawHeaders } from './network';
|
import { RawHeaders } from './network';
|
||||||
import { Headers } from './types';
|
import { FilePayload, Headers } from './types';
|
||||||
|
|
||||||
export type FetchOptions = {
|
export type FetchOptions = {
|
||||||
params?: { [key: string]: string; },
|
params?: { [key: string]: string; },
|
||||||
method?: string,
|
method?: string,
|
||||||
headers?: Headers,
|
headers?: Headers,
|
||||||
data?: string | Buffer,
|
data?: string | Buffer | Serializable,
|
||||||
timeout?: number,
|
timeout?: number,
|
||||||
failOnStatusCode?: boolean,
|
failOnStatusCode?: boolean,
|
||||||
};
|
};
|
||||||
@ -67,7 +71,7 @@ export class FetchRequest extends ChannelOwner<channels.FetchRequestChannel, cha
|
|||||||
options?: {
|
options?: {
|
||||||
params?: { [key: string]: string; };
|
params?: { [key: string]: string; };
|
||||||
headers?: { [key: string]: string; };
|
headers?: { [key: string]: string; };
|
||||||
data?: string | Buffer;
|
data?: string | Buffer | Serializable;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
failOnStatusCode?: boolean;
|
failOnStatusCode?: boolean;
|
||||||
}): Promise<FetchResponse> {
|
}): Promise<FetchResponse> {
|
||||||
@ -87,9 +91,34 @@ export class FetchRequest extends ChannelOwner<channels.FetchRequestChannel, cha
|
|||||||
// 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 postDataBuffer = isString(options.data) ? Buffer.from(options.data, 'utf8') : options.data;
|
let formData: any;
|
||||||
if (postDataBuffer === undefined)
|
let postDataBuffer: Buffer | undefined;
|
||||||
postDataBuffer = request?.postDataBuffer() || undefined;
|
if (options.data) {
|
||||||
|
if (isString(options.data)) {
|
||||||
|
postDataBuffer = Buffer.from(options.data, 'utf8');
|
||||||
|
} 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 ReadStream) {
|
||||||
|
formData[name] = await readStreamToJson(value as ReadStream);
|
||||||
|
} else {
|
||||||
|
formData[name] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unexpected 'data' type`);
|
||||||
|
}
|
||||||
|
if (postDataBuffer === undefined && formData === 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,
|
||||||
@ -97,11 +126,12 @@ export class FetchRequest extends ChannelOwner<channels.FetchRequestChannel, cha
|
|||||||
method,
|
method,
|
||||||
headers,
|
headers,
|
||||||
postData,
|
postData,
|
||||||
|
formData,
|
||||||
timeout: options.timeout,
|
timeout: options.timeout,
|
||||||
failOnStatusCode: options.failOnStatusCode,
|
failOnStatusCode: options.failOnStatusCode,
|
||||||
});
|
});
|
||||||
if (result.error)
|
if (result.error)
|
||||||
throw new Error(`Request failed: ${result.error}`);
|
throw new Error(result.error);
|
||||||
return new FetchResponse(this, result.response!);
|
return new FetchResponse(this, result.response!);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -177,3 +207,32 @@ export class FetchResponse implements api.FetchResponse {
|
|||||||
return this._initializer.fetchUid;
|
return this._initializer.fetchUid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ServerFilePayload = {
|
||||||
|
name: string,
|
||||||
|
mimeType: string,
|
||||||
|
buffer: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
function filePayloadToJson(payload: FilePayload): ServerFilePayload {
|
||||||
|
return {
|
||||||
|
name: payload.name,
|
||||||
|
mimeType: payload.mimeType,
|
||||||
|
buffer: payload.buffer.toString('base64'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readStreamToJson(stream: ReadStream): Promise<ServerFilePayload> {
|
||||||
|
const buffer = await new Promise<Buffer>((resolve, reject) => {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
stream.on('data', chunk => chunks.push(chunk));
|
||||||
|
stream.on('end', () => resolve(Buffer.concat(chunks)));
|
||||||
|
stream.on('error', err => reject(err));
|
||||||
|
});
|
||||||
|
const streamPath: string = Buffer.isBuffer(stream.path) ? stream.path.toString('utf8') : stream.path;
|
||||||
|
return {
|
||||||
|
name: path.basename(streamPath),
|
||||||
|
mimeType: mime.getType(streamPath) || 'application/octet-stream',
|
||||||
|
buffer: buffer.toString('base64'),
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -188,6 +188,7 @@ export class FetchRequestDispatcher extends Dispatcher<FetchRequest, channels.Fe
|
|||||||
method: params.method,
|
method: params.method,
|
||||||
headers: params.headers ? headersArrayToObject(params.headers, false) : undefined,
|
headers: params.headers ? headersArrayToObject(params.headers, false) : undefined,
|
||||||
postData: params.postData ? Buffer.from(params.postData, 'base64') : undefined,
|
postData: params.postData ? Buffer.from(params.postData, 'base64') : undefined,
|
||||||
|
formData: params.formData,
|
||||||
timeout: params.timeout,
|
timeout: params.timeout,
|
||||||
failOnStatusCode: params.failOnStatusCode,
|
failOnStatusCode: params.failOnStatusCode,
|
||||||
});
|
});
|
||||||
@ -213,4 +214,3 @@ export class FetchRequestDispatcher extends Dispatcher<FetchRequest, channels.Fe
|
|||||||
this._object.fetchResponses.delete(params.fetchUid);
|
this._object.fetchResponses.delete(params.fetchUid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -164,6 +164,7 @@ export type FetchRequestFetchParams = {
|
|||||||
method?: string,
|
method?: string,
|
||||||
headers?: NameValue[],
|
headers?: NameValue[],
|
||||||
postData?: Binary,
|
postData?: Binary,
|
||||||
|
formData?: any,
|
||||||
timeout?: number,
|
timeout?: number,
|
||||||
failOnStatusCode?: boolean,
|
failOnStatusCode?: boolean,
|
||||||
};
|
};
|
||||||
@ -172,6 +173,7 @@ export type FetchRequestFetchOptions = {
|
|||||||
method?: string,
|
method?: string,
|
||||||
headers?: NameValue[],
|
headers?: NameValue[],
|
||||||
postData?: Binary,
|
postData?: Binary,
|
||||||
|
formData?: any,
|
||||||
timeout?: number,
|
timeout?: number,
|
||||||
failOnStatusCode?: boolean,
|
failOnStatusCode?: boolean,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -233,6 +233,7 @@ FetchRequest:
|
|||||||
type: array?
|
type: array?
|
||||||
items: NameValue
|
items: NameValue
|
||||||
postData: binary?
|
postData: binary?
|
||||||
|
formData: json?
|
||||||
timeout: number?
|
timeout: number?
|
||||||
failOnStatusCode: boolean?
|
failOnStatusCode: boolean?
|
||||||
returns:
|
returns:
|
||||||
|
|||||||
@ -153,6 +153,7 @@ 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),
|
||||||
timeout: tOptional(tNumber),
|
timeout: tOptional(tNumber),
|
||||||
failOnStatusCode: tOptional(tBoolean),
|
failOnStatusCode: tOptional(tBoolean),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -22,12 +22,13 @@ import * as https from 'https';
|
|||||||
import { BrowserContext } from './browserContext';
|
import { BrowserContext } from './browserContext';
|
||||||
import * as types from './types';
|
import * as types from './types';
|
||||||
import { pipeline, Readable, Transform } from 'stream';
|
import { pipeline, Readable, Transform } from 'stream';
|
||||||
import { createGuid, monotonicTime } from '../utils/utils';
|
import { createGuid, isFilePayload, monotonicTime } from '../utils/utils';
|
||||||
import { SdkObject } from './instrumentation';
|
import { SdkObject } from './instrumentation';
|
||||||
import { Playwright } from './playwright';
|
import { Playwright } from './playwright';
|
||||||
import { HeadersArray, ProxySettings } from './types';
|
import { HeadersArray, ProxySettings } from './types';
|
||||||
import { HTTPCredentials } from '../../types/types';
|
import { HTTPCredentials } from '../../types/types';
|
||||||
import { TimeoutSettings } from '../utils/timeoutSettings';
|
import { TimeoutSettings } from '../utils/timeoutSettings';
|
||||||
|
import { MultipartFormData } from './formData';
|
||||||
|
|
||||||
|
|
||||||
type FetchRequestOptions = {
|
type FetchRequestOptions = {
|
||||||
@ -130,7 +131,13 @@ export abstract class FetchRequest extends SdkObject {
|
|||||||
requestUrl.searchParams.set(name, value);
|
requestUrl.searchParams.set(name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchResponse = await this._sendRequest(requestUrl, options, params.postData);
|
let postData;
|
||||||
|
if (['POST', 'PUSH', 'PATCH'].includes(method))
|
||||||
|
postData = params.formData ? serilizeFormData(params.formData, headers) : params.postData;
|
||||||
|
else if (params.postData || params.formData)
|
||||||
|
throw new Error(`Method ${method} does not accept post data`);
|
||||||
|
|
||||||
|
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))
|
||||||
return { error: `${fetchResponse.status} ${fetchResponse.statusText}` };
|
return { error: `${fetchResponse.status} ${fetchResponse.statusText}` };
|
||||||
@ -410,3 +417,31 @@ function parseCookie(header: string) {
|
|||||||
}
|
}
|
||||||
return cookie;
|
return cookie;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function serilizeFormData(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;
|
||||||
|
return Buffer.from(json, 'utf8');
|
||||||
|
} else if (contentType === 'application/x-www-form-urlencoded') {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
for (const [name, value] of Object.entries(data))
|
||||||
|
searchParams.append(name, String(value));
|
||||||
|
return Buffer.from(searchParams.toString(), 'utf8');
|
||||||
|
} else if (contentType === 'multipart/form-data') {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
headers['content-type'] = formData.contentTypeHeader();
|
||||||
|
return formData.finish();
|
||||||
|
} else {
|
||||||
|
throw new Error(`Cannot serialize data using content type: ${contentType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
90
src/server/formData.ts
Normal file
90
src/server/formData.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as types from './types';
|
||||||
|
|
||||||
|
export class MultipartFormData {
|
||||||
|
private readonly _boundary: string;
|
||||||
|
private readonly _chunks: Buffer[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._boundary = generateUniqueBoundaryString();
|
||||||
|
}
|
||||||
|
|
||||||
|
contentTypeHeader() {
|
||||||
|
return `multipart/form-data; boundary=${this._boundary}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
addField(name: string, value: string) {
|
||||||
|
this._beginMultiPartHeader(name);
|
||||||
|
this._finishMultiPartHeader();
|
||||||
|
this._chunks.push(Buffer.from(value));
|
||||||
|
this._finishMultiPartField();
|
||||||
|
}
|
||||||
|
|
||||||
|
addFileField(name: string, value: types.FilePayload) {
|
||||||
|
this._beginMultiPartHeader(name);
|
||||||
|
this._chunks.push(Buffer.from(`; filename="${value.name}"`));
|
||||||
|
this._chunks.push(Buffer.from(`\r\ncontent-type: ${value.mimeType || 'application/octet-stream'}`));
|
||||||
|
this._finishMultiPartHeader();
|
||||||
|
this._chunks.push(Buffer.from(value.buffer, 'base64'));
|
||||||
|
this._finishMultiPartField();
|
||||||
|
}
|
||||||
|
|
||||||
|
finish(): Buffer {
|
||||||
|
this._addBoundary(true);
|
||||||
|
return Buffer.concat(this._chunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _beginMultiPartHeader(name: string) {
|
||||||
|
this._addBoundary();
|
||||||
|
this._chunks.push(Buffer.from(`content-disposition: form-data; name="${name}"`));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _finishMultiPartHeader() {
|
||||||
|
this._chunks.push(Buffer.from(`\r\n\r\n`));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _finishMultiPartField() {
|
||||||
|
this._chunks.push(Buffer.from(`\r\n`));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addBoundary(isLastBoundary?: boolean) {
|
||||||
|
this._chunks.push(Buffer.from('--' + this._boundary));
|
||||||
|
if (isLastBoundary)
|
||||||
|
this._chunks.push(Buffer.from('--'));
|
||||||
|
this._chunks.push(Buffer.from('\r\n'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const alphaNumericEncodingMap = [
|
||||||
|
0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,
|
||||||
|
0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50,
|
||||||
|
0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,
|
||||||
|
0x59, 0x5A, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66,
|
||||||
|
0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E,
|
||||||
|
0x6F, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76,
|
||||||
|
0x77, 0x78, 0x79, 0x7A, 0x30, 0x31, 0x32, 0x33,
|
||||||
|
0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x41, 0x42
|
||||||
|
];
|
||||||
|
|
||||||
|
// See generateUniqueBoundaryString() in WebKit
|
||||||
|
function generateUniqueBoundaryString(): string {
|
||||||
|
const charCodes = [];
|
||||||
|
for (let i = 0; i < 16; i++)
|
||||||
|
charCodes.push(alphaNumericEncodingMap[Math.floor(Math.random() * alphaNumericEncodingMap.length)]);
|
||||||
|
return '----WebKitFormBoundary' + String.fromCharCode(...charCodes);
|
||||||
|
}
|
||||||
@ -372,12 +372,25 @@ 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 = {
|
export type FetchOptions = {
|
||||||
url: string,
|
url: string,
|
||||||
params?: { [name: string]: string },
|
params?: { [name: string]: string },
|
||||||
method?: string,
|
method?: string,
|
||||||
headers?: { [name: string]: string },
|
headers?: { [name: string]: string },
|
||||||
postData?: Buffer,
|
postData?: Buffer,
|
||||||
|
formData?: FormField[],
|
||||||
timeout?: number,
|
timeout?: number,
|
||||||
failOnStatusCode?: boolean,
|
failOnStatusCode?: boolean,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -411,3 +411,7 @@ export function wrapInASCIIBox(text: string, padding = 0): string {
|
|||||||
'╚' + '═'.repeat(maxLength + padding * 2) + '╝',
|
'╚' + '═'.repeat(maxLength + padding * 2) + '╝',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isFilePayload(value: any): boolean {
|
||||||
|
return typeof value === 'object' && value['name'] && value['mimeType'] && value['buffer'];
|
||||||
|
}
|
||||||
|
|||||||
@ -14,8 +14,10 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import formidable from 'formidable';
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
import zlib from 'zlib';
|
import zlib from 'zlib';
|
||||||
|
import fs from 'fs';
|
||||||
import { pipeline } from 'stream';
|
import { pipeline } from 'stream';
|
||||||
import { contextTest as it, expect } from './config/browserTest';
|
import { contextTest as it, expect } from './config/browserTest';
|
||||||
import { suppressCertificateWarning } from './config/utils';
|
import { suppressCertificateWarning } from './config/utils';
|
||||||
@ -166,7 +168,7 @@ for (const method of ['get', 'post', 'fetch']) {
|
|||||||
const error = await context._request[method](server.PREFIX + '/does-not-exist.html', {
|
const error = await context._request[method](server.PREFIX + '/does-not-exist.html', {
|
||||||
failOnStatusCode: true
|
failOnStatusCode: true
|
||||||
}).catch(e => e);
|
}).catch(e => e);
|
||||||
expect(error.message).toContain('Request failed: 404 Not Found');
|
expect(error.message).toContain('404 Not Found');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -705,3 +707,156 @@ it('should override request parameters', async function({context, page, server})
|
|||||||
expect(req.headers.foo).toBe('bar');
|
expect(req.headers.foo).toBe('bar');
|
||||||
expect((await req.postBody).toString('utf8')).toBe('data');
|
expect((await req.postBody).toString('utf8')).toBe('data');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support application/x-www-form-urlencoded', async function({context, page, server}) {
|
||||||
|
const [req] = await Promise.all([
|
||||||
|
server.waitForRequest('/empty.html'),
|
||||||
|
context._request.post(server.EMPTY_PAGE, {
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
file: 'f.js',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
expect(req.method).toBe('POST');
|
||||||
|
expect(req.headers['content-type']).toBe('application/x-www-form-urlencoded');
|
||||||
|
const body = (await req.postBody).toString('utf8');
|
||||||
|
const params = new URLSearchParams(body);
|
||||||
|
expect(params.get('firstName')).toBe('John');
|
||||||
|
expect(params.get('lastName')).toBe('Doe');
|
||||||
|
expect(params.get('file')).toBe('f.js');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should encode to application/json by default', async function({context, page, server}) {
|
||||||
|
const data = {
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
file: {
|
||||||
|
name: 'f.js'
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const [req] = await Promise.all([
|
||||||
|
server.waitForRequest('/empty.html'),
|
||||||
|
context._request.post(server.EMPTY_PAGE, { data })
|
||||||
|
]);
|
||||||
|
expect(req.method).toBe('POST');
|
||||||
|
expect(req.headers['content-type']).toBe('application/json');
|
||||||
|
const body = (await req.postBody).toString('utf8');
|
||||||
|
const json = JSON.parse(body);
|
||||||
|
expect(json).toEqual(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support multipart/form-data', async function({context, page, server}) {
|
||||||
|
const formReceived = new Promise<any>(resolve => {
|
||||||
|
server.setRoute('/empty.html', async (serverRequest, res) => {
|
||||||
|
const form = new formidable.IncomingForm();
|
||||||
|
form.parse(serverRequest, (error, fields, files) => {
|
||||||
|
server.serveFile(serverRequest, res);
|
||||||
|
resolve({error, fields, files, serverRequest });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const file = {
|
||||||
|
name: 'f.js',
|
||||||
|
mimeType: 'text/javascript',
|
||||||
|
buffer: Buffer.from('var x = 10;\r\n;console.log(x);')
|
||||||
|
};
|
||||||
|
const [{error, fields, files, serverRequest}, response] = await Promise.all([
|
||||||
|
formReceived,
|
||||||
|
context._request.post(server.EMPTY_PAGE, {
|
||||||
|
headers: {
|
||||||
|
'content-type': 'multipart/form-data'
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
file
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
expect(error).toBeFalsy();
|
||||||
|
expect(serverRequest.method).toBe('POST');
|
||||||
|
expect(serverRequest.headers['content-type']).toContain('multipart/form-data');
|
||||||
|
expect(fields['firstName']).toBe('John');
|
||||||
|
expect(fields['lastName']).toBe('Doe');
|
||||||
|
expect(files['file'].name).toBe(file.name);
|
||||||
|
expect(files['file'].type).toBe(file.mimeType);
|
||||||
|
expect(fs.readFileSync(files['file'].path).toString()).toBe(file.buffer.toString('utf8'));
|
||||||
|
expect(response.status()).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support multipart/form-data with ReadSream values', async function({context, page, asset, server}) {
|
||||||
|
const formReceived = new Promise<any>(resolve => {
|
||||||
|
server.setRoute('/empty.html', async (serverRequest, res) => {
|
||||||
|
const form = new formidable.IncomingForm();
|
||||||
|
form.parse(serverRequest, (error, fields, files) => {
|
||||||
|
server.serveFile(serverRequest, res);
|
||||||
|
resolve({error, fields, files, serverRequest });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const readStream = fs.createReadStream(asset('simplezip.json'));
|
||||||
|
const [{error, fields, files, serverRequest}, response] = await Promise.all([
|
||||||
|
formReceived,
|
||||||
|
context._request.post(server.EMPTY_PAGE, {
|
||||||
|
headers: {
|
||||||
|
'content-type': 'multipart/form-data'
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
readStream
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
expect(error).toBeFalsy();
|
||||||
|
expect(serverRequest.method).toBe('POST');
|
||||||
|
expect(serverRequest.headers['content-type']).toContain('multipart/form-data');
|
||||||
|
expect(fields['firstName']).toBe('John');
|
||||||
|
expect(fields['lastName']).toBe('Doe');
|
||||||
|
expect(files['readStream'].name).toBe('simplezip.json');
|
||||||
|
expect(files['readStream'].type).toBe('application/json');
|
||||||
|
expect(fs.readFileSync(files['readStream'].path).toString()).toBe(fs.readFileSync(asset('simplezip.json')).toString());
|
||||||
|
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 throw nice error on unsupported data type', async function({context, server}) {
|
||||||
|
const error = await context._request.post(server.EMPTY_PAGE, {
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json'
|
||||||
|
},
|
||||||
|
data: () => true
|
||||||
|
}).catch(e => e);
|
||||||
|
expect(error.message).toContain(`Unexpected 'data' type`);
|
||||||
|
});
|
||||||
|
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
}).catch(e => e);
|
||||||
|
expect(error.message).toContain(`Method GET does not accept post data`);
|
||||||
|
});
|
||||||
|
|||||||
18
types/types.d.ts
vendored
18
types/types.d.ts
vendored
@ -12646,9 +12646,14 @@ export interface FetchRequest {
|
|||||||
*/
|
*/
|
||||||
fetch(urlOrRequest: string|Request, options?: {
|
fetch(urlOrRequest: string|Request, options?: {
|
||||||
/**
|
/**
|
||||||
* Allows to set post data of the fetch.
|
* 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`.
|
||||||
*/
|
*/
|
||||||
data?: string|Buffer;
|
data?: string|Buffer|Serializable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes.
|
* Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes.
|
||||||
@ -12712,9 +12717,14 @@ export interface FetchRequest {
|
|||||||
*/
|
*/
|
||||||
post(urlOrRequest: string|Request, options?: {
|
post(urlOrRequest: string|Request, options?: {
|
||||||
/**
|
/**
|
||||||
* Allows to set post data of the fetch.
|
* 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`.
|
||||||
*/
|
*/
|
||||||
data?: string|Buffer;
|
data?: string|Buffer|Serializable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes.
|
* Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user