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-%%
* 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
### 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-%%
* 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
### 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-%%
* 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
### 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-%%
* 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
### 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-%%
* 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
### 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-%%
* 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
### 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-%%
* 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
### 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
for all status codes.
## js-python-fetch-option-form
* langs: js, python
## js-fetch-option-form
* 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]>>
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,
headers?: Headers,
data?: string | Buffer | Serializable,
form?: { [key: string]: string|number|boolean; };
multipart?: { [key: string]: string|number|boolean|fs.ReadStream|FilePayload; };
form?: { [key: string]: string|number|boolean; } | FormData;
multipart?: { [key: string]: string|number|boolean|fs.ReadStream|FilePayload; } | FormData;
timeout?: number,
failOnStatusCode?: boolean,
ignoreHTTPSErrors?: boolean,
@ -202,7 +202,16 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
throw new Error(`Unexpected 'data' type`);
}
} 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) {
multipartData = [];
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
* `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
@ -16689,7 +16689,7 @@ export interface APIRequestContext {
* 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; };
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
@ -16807,7 +16807,7 @@ export interface APIRequestContext {
* 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; };
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
@ -16893,7 +16893,7 @@ export interface APIRequestContext {
* 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; };
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
@ -16979,7 +16979,7 @@ export interface APIRequestContext {
* 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; };
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
@ -17107,7 +17107,7 @@ export interface APIRequestContext {
* 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; };
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
@ -17193,7 +17193,7 @@ export interface APIRequestContext {
* 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; };
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

View file

@ -960,6 +960,22 @@ it('should support application/x-www-form-urlencoded', async function({ context,
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 }) {
const data = {
firstName: 'John',