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.
 | 
			
		||||
 | 
			
		||||
### 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
 | 
			
		||||
- `timeout` <[float]>
 | 
			
		||||
@ -108,9 +111,12 @@ Query parameters to be send with the URL.
 | 
			
		||||
Allows to set HTTP headers.
 | 
			
		||||
 | 
			
		||||
### 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
 | 
			
		||||
- `timeout` <[float]>
 | 
			
		||||
 | 
			
		||||
@ -14,21 +14,25 @@
 | 
			
		||||
 * 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 { HeadersArray } from '../common/types';
 | 
			
		||||
import * as channels from '../protocol/channels';
 | 
			
		||||
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 * as network from './network';
 | 
			
		||||
import { RawHeaders } from './network';
 | 
			
		||||
import { Headers } from './types';
 | 
			
		||||
import { FilePayload, Headers } from './types';
 | 
			
		||||
 | 
			
		||||
export type FetchOptions = {
 | 
			
		||||
  params?: { [key: string]: string; },
 | 
			
		||||
  method?: string,
 | 
			
		||||
  headers?: Headers,
 | 
			
		||||
  data?: string | Buffer,
 | 
			
		||||
  data?: string | Buffer | Serializable,
 | 
			
		||||
  timeout?: number,
 | 
			
		||||
  failOnStatusCode?: boolean,
 | 
			
		||||
};
 | 
			
		||||
@ -67,7 +71,7 @@ export class FetchRequest extends ChannelOwner<channels.FetchRequestChannel, cha
 | 
			
		||||
    options?: {
 | 
			
		||||
      params?: { [key: string]: string; };
 | 
			
		||||
      headers?: { [key: string]: string; };
 | 
			
		||||
      data?: string | Buffer;
 | 
			
		||||
      data?: string | Buffer | Serializable;
 | 
			
		||||
      timeout?: number;
 | 
			
		||||
      failOnStatusCode?: boolean;
 | 
			
		||||
    }): 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.
 | 
			
		||||
      const headersObj = options.headers || request?.headers() ;
 | 
			
		||||
      const headers = headersObj ? headersObjectToArray(headersObj) : undefined;
 | 
			
		||||
      let postDataBuffer = isString(options.data) ? Buffer.from(options.data, 'utf8') : options.data;
 | 
			
		||||
      if (postDataBuffer === undefined)
 | 
			
		||||
        postDataBuffer = request?.postDataBuffer() || undefined;
 | 
			
		||||
      let formData: any;
 | 
			
		||||
      let postDataBuffer: Buffer | 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 result = await channel.fetch({
 | 
			
		||||
        url,
 | 
			
		||||
@ -97,11 +126,12 @@ export class FetchRequest extends ChannelOwner<channels.FetchRequestChannel, cha
 | 
			
		||||
        method,
 | 
			
		||||
        headers,
 | 
			
		||||
        postData,
 | 
			
		||||
        formData,
 | 
			
		||||
        timeout: options.timeout,
 | 
			
		||||
        failOnStatusCode: options.failOnStatusCode,
 | 
			
		||||
      });
 | 
			
		||||
      if (result.error)
 | 
			
		||||
        throw new Error(`Request failed: ${result.error}`);
 | 
			
		||||
        throw new Error(result.error);
 | 
			
		||||
      return new FetchResponse(this, result.response!);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
@ -177,3 +207,32 @@ export class FetchResponse implements api.FetchResponse {
 | 
			
		||||
    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,
 | 
			
		||||
      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,
 | 
			
		||||
    });
 | 
			
		||||
@ -213,4 +214,3 @@ export class FetchRequestDispatcher extends Dispatcher<FetchRequest, channels.Fe
 | 
			
		||||
    this._object.fetchResponses.delete(params.fetchUid);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -164,6 +164,7 @@ export type FetchRequestFetchParams = {
 | 
			
		||||
  method?: string,
 | 
			
		||||
  headers?: NameValue[],
 | 
			
		||||
  postData?: Binary,
 | 
			
		||||
  formData?: any,
 | 
			
		||||
  timeout?: number,
 | 
			
		||||
  failOnStatusCode?: boolean,
 | 
			
		||||
};
 | 
			
		||||
@ -172,6 +173,7 @@ export type FetchRequestFetchOptions = {
 | 
			
		||||
  method?: string,
 | 
			
		||||
  headers?: NameValue[],
 | 
			
		||||
  postData?: Binary,
 | 
			
		||||
  formData?: any,
 | 
			
		||||
  timeout?: number,
 | 
			
		||||
  failOnStatusCode?: boolean,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -233,6 +233,7 @@ FetchRequest:
 | 
			
		||||
          type: array?
 | 
			
		||||
          items: NameValue
 | 
			
		||||
        postData: binary?
 | 
			
		||||
        formData: json?
 | 
			
		||||
        timeout: number?
 | 
			
		||||
        failOnStatusCode: boolean?
 | 
			
		||||
      returns:
 | 
			
		||||
 | 
			
		||||
@ -153,6 +153,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
 | 
			
		||||
    method: tOptional(tString),
 | 
			
		||||
    headers: tOptional(tArray(tType('NameValue'))),
 | 
			
		||||
    postData: tOptional(tBinary),
 | 
			
		||||
    formData: tOptional(tAny),
 | 
			
		||||
    timeout: tOptional(tNumber),
 | 
			
		||||
    failOnStatusCode: tOptional(tBoolean),
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
@ -22,12 +22,13 @@ import * as https from 'https';
 | 
			
		||||
import { BrowserContext } from './browserContext';
 | 
			
		||||
import * as types from './types';
 | 
			
		||||
import { pipeline, Readable, Transform } from 'stream';
 | 
			
		||||
import { createGuid, monotonicTime } from '../utils/utils';
 | 
			
		||||
import { createGuid, isFilePayload, monotonicTime } from '../utils/utils';
 | 
			
		||||
import { SdkObject } from './instrumentation';
 | 
			
		||||
import { Playwright } from './playwright';
 | 
			
		||||
import { HeadersArray, ProxySettings } from './types';
 | 
			
		||||
import { HTTPCredentials } from '../../types/types';
 | 
			
		||||
import { TimeoutSettings } from '../utils/timeoutSettings';
 | 
			
		||||
import { MultipartFormData } from './formData';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type FetchRequestOptions = {
 | 
			
		||||
@ -130,7 +131,13 @@ export abstract class FetchRequest extends SdkObject {
 | 
			
		||||
          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);
 | 
			
		||||
      if (params.failOnStatusCode && (fetchResponse.status < 200 || fetchResponse.status >= 400))
 | 
			
		||||
        return { error: `${fetchResponse.status} ${fetchResponse.statusText}` };
 | 
			
		||||
@ -410,3 +417,31 @@ function parseCookie(header: string) {
 | 
			
		||||
  }
 | 
			
		||||
  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[]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
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,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -411,3 +411,7 @@ export function wrapInASCIIBox(text: string, padding = 0): string {
 | 
			
		||||
    '╚' + '═'.repeat(maxLength + padding * 2) + '╝',
 | 
			
		||||
  ].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.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import formidable from 'formidable';
 | 
			
		||||
import http from 'http';
 | 
			
		||||
import zlib from 'zlib';
 | 
			
		||||
import fs from 'fs';
 | 
			
		||||
import { pipeline } from 'stream';
 | 
			
		||||
import { contextTest as it, expect } from './config/browserTest';
 | 
			
		||||
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', {
 | 
			
		||||
      failOnStatusCode: true
 | 
			
		||||
    }).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((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?: {
 | 
			
		||||
    /**
 | 
			
		||||
     * 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.
 | 
			
		||||
@ -12712,9 +12717,14 @@ export interface FetchRequest {
 | 
			
		||||
   */
 | 
			
		||||
  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.
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user