feat(library): accept FormData in fetch (#32602)

Closes https://github.com/microsoft/playwright/issues/26520 by accepting
`FormData`, which became stable in Node.js in v21.
This commit is contained in:
Simon Knott 2024-09-13 13:21:02 +02:00 committed by GitHub
parent cd4dabef8b
commit 48c7fb6b06
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 73 additions and 19 deletions

View file

@ -159,7 +159,10 @@ context cookies from the response. The method will automatically follow redirect
### option: APIRequestContext.delete.data = %%-js-python-csharp-fetch-option-data-%% ### option: APIRequestContext.delete.data = %%-js-python-csharp-fetch-option-data-%%
* since: v1.17 * since: v1.17
### option: APIRequestContext.delete.form = %%-js-python-fetch-option-form-%% ### option: APIRequestContext.delete.form = %%-js-fetch-option-form-%%
* since: v1.17
### option: APIRequestContext.delete.form = %%-python-fetch-option-form-%%
* since: v1.17 * since: v1.17
### option: APIRequestContext.delete.form = %%-csharp-fetch-option-form-%% ### option: APIRequestContext.delete.form = %%-csharp-fetch-option-form-%%
@ -332,7 +335,10 @@ If set changes the fetch method (e.g. [PUT](https://developer.mozilla.org/en-US/
### option: APIRequestContext.fetch.data = %%-js-python-csharp-fetch-option-data-%% ### option: APIRequestContext.fetch.data = %%-js-python-csharp-fetch-option-data-%%
* since: v1.16 * since: v1.16
### option: APIRequestContext.fetch.form = %%-js-python-fetch-option-form-%% ### option: APIRequestContext.fetch.form = %%-js-fetch-option-form-%%
* since: v1.16
### option: APIRequestContext.fetch.form = %%-python-fetch-option-form-%%
* since: v1.16 * since: v1.16
### option: APIRequestContext.fetch.form = %%-csharp-fetch-option-form-%% ### option: APIRequestContext.fetch.form = %%-csharp-fetch-option-form-%%
@ -442,7 +448,10 @@ await request.GetAsync("https://example.com/api/getText", new() { Params = query
### option: APIRequestContext.get.data = %%-js-python-csharp-fetch-option-data-%% ### option: APIRequestContext.get.data = %%-js-python-csharp-fetch-option-data-%%
* since: v1.26 * since: v1.26
### option: APIRequestContext.get.form = %%-js-python-fetch-option-form-%% ### option: APIRequestContext.get.form = %%-js-fetch-option-form-%%
* since: v1.26
### option: APIRequestContext.get.form = %%-python-fetch-option-form-%%
* since: v1.26 * since: v1.26
### option: APIRequestContext.get.form = %%-csharp-fetch-option-form-%% ### option: APIRequestContext.get.form = %%-csharp-fetch-option-form-%%
@ -504,7 +513,10 @@ context cookies from the response. The method will automatically follow redirect
### option: APIRequestContext.head.data = %%-js-python-csharp-fetch-option-data-%% ### option: APIRequestContext.head.data = %%-js-python-csharp-fetch-option-data-%%
* since: v1.26 * since: v1.26
### option: APIRequestContext.head.form = %%-js-python-fetch-option-form-%% ### option: APIRequestContext.head.form = %%-python-fetch-option-form-%%
* since: v1.26
### option: APIRequestContext.head.form = %%-js-fetch-option-form-%%
* since: v1.26 * since: v1.26
### option: APIRequestContext.head.form = %%-csharp-fetch-option-form-%% ### option: APIRequestContext.head.form = %%-csharp-fetch-option-form-%%
@ -566,7 +578,10 @@ context cookies from the response. The method will automatically follow redirect
### option: APIRequestContext.patch.data = %%-js-python-csharp-fetch-option-data-%% ### option: APIRequestContext.patch.data = %%-js-python-csharp-fetch-option-data-%%
* since: v1.16 * since: v1.16
### option: APIRequestContext.patch.form = %%-js-python-fetch-option-form-%% ### option: APIRequestContext.patch.form = %%-js-fetch-option-form-%%
* since: v1.16
### option: APIRequestContext.patch.form = %%-python-fetch-option-form-%%
* since: v1.16 * since: v1.16
### option: APIRequestContext.patch.form = %%-csharp-fetch-option-form-%% ### option: APIRequestContext.patch.form = %%-csharp-fetch-option-form-%%
@ -749,7 +764,10 @@ await request.PostAsync("https://example.com/api/uploadScript", new() { Multipar
### option: APIRequestContext.post.data = %%-js-python-csharp-fetch-option-data-%% ### option: APIRequestContext.post.data = %%-js-python-csharp-fetch-option-data-%%
* since: v1.16 * since: v1.16
### option: APIRequestContext.post.form = %%-js-python-fetch-option-form-%% ### option: APIRequestContext.post.form = %%-js-fetch-option-form-%%
* since: v1.16
### option: APIRequestContext.post.form = %%-python-fetch-option-form-%%
* since: v1.16 * since: v1.16
### option: APIRequestContext.post.form = %%-csharp-fetch-option-form-%% ### option: APIRequestContext.post.form = %%-csharp-fetch-option-form-%%
@ -811,7 +829,10 @@ context cookies from the response. The method will automatically follow redirect
### option: APIRequestContext.put.data = %%-js-python-csharp-fetch-option-data-%% ### option: APIRequestContext.put.data = %%-js-python-csharp-fetch-option-data-%%
* since: v1.16 * since: v1.16
### option: APIRequestContext.put.form = %%-js-python-fetch-option-form-%% ### option: APIRequestContext.put.form = %%-python-fetch-option-form-%%
* since: v1.16
### option: APIRequestContext.put.form = %%-js-fetch-option-form-%%
* since: v1.16 * since: v1.16
### option: APIRequestContext.put.form = %%-csharp-fetch-option-form-%% ### option: APIRequestContext.put.form = %%-csharp-fetch-option-form-%%

View file

@ -405,8 +405,16 @@ Request timeout in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to d
Whether to throw on response codes other than 2xx and 3xx. By default response object is returned Whether to throw on response codes other than 2xx and 3xx. By default response object is returned
for all status codes. for all status codes.
## js-python-fetch-option-form ## js-fetch-option-form
* langs: js, python * langs: js
- `form` <[Object]<[string], [string]|[float]|[boolean]>|[FormData]>
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.
## python-fetch-option-form
* langs: python
- `form` <[Object]<[string], [string]|[float]|[boolean]>> - `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 Provides an object that will be serialized as html form using `application/x-www-form-urlencoded` encoding and sent as

View file

@ -36,8 +36,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; }; form?: { [key: string]: string|number|boolean; } | FormData;
multipart?: { [key: string]: string|number|boolean|fs.ReadStream|FilePayload; }; multipart?: { [key: string]: string|number|boolean|fs.ReadStream|FilePayload; } | FormData;
timeout?: number, timeout?: number,
failOnStatusCode?: boolean, failOnStatusCode?: boolean,
ignoreHTTPSErrors?: boolean, ignoreHTTPSErrors?: boolean,
@ -202,7 +202,16 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
throw new Error(`Unexpected 'data' type`); throw new Error(`Unexpected 'data' type`);
} }
} else if (options.form) { } else if (options.form) {
formData = objectToArray(options.form); if (globalThis.FormData && options.form instanceof FormData) {
formData = [];
for (const [name, value] of options.form.entries()) {
if (typeof value !== 'string')
throw new Error(`Expected string for options.form["${name}"], found File. Please use options.multipart instead.`);
formData.push({ name, value });
}
} else {
formData = objectToArray(options.form);
}
} else if (options.multipart) { } else if (options.multipart) {
multipartData = []; multipartData = [];
if (globalThis.FormData && options.multipart instanceof FormData) { if (globalThis.FormData && options.multipart instanceof FormData) {

View file

@ -16559,7 +16559,7 @@ export interface APIRequestContext {
* as this request body. If this parameter is specified `content-type` header will be set to * as this request body. If this parameter is specified `content-type` header will be set to
* `application/x-www-form-urlencoded` unless explicitly provided. * `application/x-www-form-urlencoded` unless explicitly provided.
*/ */
form?: { [key: string]: string|number|boolean; }; form?: { [key: string]: string|number|boolean; }|FormData;
/** /**
* Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by * Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by
@ -16689,7 +16689,7 @@ export interface APIRequestContext {
* as this request body. If this parameter is specified `content-type` header will be set to * as this request body. If this parameter is specified `content-type` header will be set to
* `application/x-www-form-urlencoded` unless explicitly provided. * `application/x-www-form-urlencoded` unless explicitly provided.
*/ */
form?: { [key: string]: string|number|boolean; }; form?: { [key: string]: string|number|boolean; }|FormData;
/** /**
* Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by * Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by
@ -16807,7 +16807,7 @@ export interface APIRequestContext {
* as this request body. If this parameter is specified `content-type` header will be set to * as this request body. If this parameter is specified `content-type` header will be set to
* `application/x-www-form-urlencoded` unless explicitly provided. * `application/x-www-form-urlencoded` unless explicitly provided.
*/ */
form?: { [key: string]: string|number|boolean; }; form?: { [key: string]: string|number|boolean; }|FormData;
/** /**
* Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by * Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by
@ -16893,7 +16893,7 @@ export interface APIRequestContext {
* as this request body. If this parameter is specified `content-type` header will be set to * as this request body. If this parameter is specified `content-type` header will be set to
* `application/x-www-form-urlencoded` unless explicitly provided. * `application/x-www-form-urlencoded` unless explicitly provided.
*/ */
form?: { [key: string]: string|number|boolean; }; form?: { [key: string]: string|number|boolean; }|FormData;
/** /**
* Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by * Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by
@ -16979,7 +16979,7 @@ export interface APIRequestContext {
* as this request body. If this parameter is specified `content-type` header will be set to * as this request body. If this parameter is specified `content-type` header will be set to
* `application/x-www-form-urlencoded` unless explicitly provided. * `application/x-www-form-urlencoded` unless explicitly provided.
*/ */
form?: { [key: string]: string|number|boolean; }; form?: { [key: string]: string|number|boolean; }|FormData;
/** /**
* Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by * Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by
@ -17107,7 +17107,7 @@ export interface APIRequestContext {
* as this request body. If this parameter is specified `content-type` header will be set to * as this request body. If this parameter is specified `content-type` header will be set to
* `application/x-www-form-urlencoded` unless explicitly provided. * `application/x-www-form-urlencoded` unless explicitly provided.
*/ */
form?: { [key: string]: string|number|boolean; }; form?: { [key: string]: string|number|boolean; }|FormData;
/** /**
* Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by * Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by
@ -17193,7 +17193,7 @@ export interface APIRequestContext {
* as this request body. If this parameter is specified `content-type` header will be set to * as this request body. If this parameter is specified `content-type` header will be set to
* `application/x-www-form-urlencoded` unless explicitly provided. * `application/x-www-form-urlencoded` unless explicitly provided.
*/ */
form?: { [key: string]: string|number|boolean; }; form?: { [key: string]: string|number|boolean; }|FormData;
/** /**
* Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by * Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by

View file

@ -960,6 +960,22 @@ it('should support application/x-www-form-urlencoded', async function({ context,
expect(params.get('file')).toBe('f.js'); expect(params.get('file')).toBe('f.js');
}); });
it('should support application/x-www-form-urlencoded with param lists', async function({ context, page, server }) {
const form = new FormData();
form.append('foo', '1');
form.append('foo', '2');
const [req] = await Promise.all([
server.waitForRequest('/empty.html'),
context.request.post(server.EMPTY_PAGE, { form })
]);
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(req.headers['content-length']).toBe(String(params.toString().length));
expect(params.getAll('foo')).toEqual(['1', '2']);
});
it('should encode to application/json by default', async function({ context, page, server }) { it('should encode to application/json by default', async function({ context, page, server }) {
const data = { const data = {
firstName: 'John', firstName: 'John',