diff --git a/docs/src/api/class-apirequestcontext.md b/docs/src/api/class-apirequestcontext.md index dacd5dd343..addf5baacb 100644 --- a/docs/src/api/class-apirequestcontext.md +++ b/docs/src/api/class-apirequestcontext.md @@ -566,7 +566,7 @@ api_request_context.post("https://example.com/api/createBook", data=data) ```csharp var data = new Dictionary() { - { "firstNam", "John" }, + { "firstName", "John" }, { "lastName", "Doe" } }; await request.PostAsync("https://example.com/api/createBook", new() { DataObject = data }); diff --git a/docs/src/api/params.md b/docs/src/api/params.md index e3b2894c3c..e64c77f1dc 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -405,7 +405,7 @@ An instance of [FormData] can be created via [`method: APIRequestContext.createF ## js-python-fetch-option-multipart * langs: js, python -- `multipart` <[Object]<[string], [string]|[float]|[boolean]|[ReadStream]|[Object]>> +- `multipart` <[Object]<[string], [string]|[float]|[boolean]|[ReadStream]|[Object]|Array<[string]|[float]|[boolean]|[ReadStream]|[Object]>>> - `name` <[string]> File name - `mimeType` <[string]> File type - `buffer` <[Buffer]> File content @@ -413,7 +413,8 @@ An instance of [FormData] can be created via [`method: APIRequestContext.createF 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. +or as file-like object containing file name, mime-type and its content. If the value is an array, each element +will be sent as a separate field with the same name. ## csharp-fetch-option-multipart * langs: csharp diff --git a/packages/playwright-core/src/client/fetch.ts b/packages/playwright-core/src/client/fetch.ts index 8dc3caa570..f959b47fb0 100644 --- a/packages/playwright-core/src/client/fetch.ts +++ b/packages/playwright-core/src/client/fetch.ts @@ -36,7 +36,7 @@ export type FetchOptions = { headers?: Headers, data?: string | Buffer | Serializable, form?: { [key: string]: string|number|boolean; }; - multipart?: { [key: string]: string|number|boolean|fs.ReadStream|FilePayload; }; + multipart?: { [key: string]: string|number|boolean|fs.ReadStream|FilePayload|Array; }; timeout?: number, failOnStatusCode?: boolean, ignoreHTTPSErrors?: boolean, @@ -188,15 +188,11 @@ export class APIRequestContext extends ChannelOwner { + if (isFilePayload(value)) { + const payload = value as FilePayload; + if (!Buffer.isBuffer(payload.buffer)) + throw new Error(`Unexpected buffer type of 'data.${name}'`); + return { name, file: filePayloadToJson(payload) }; + } else if (value instanceof fs.ReadStream) { + return { name, file: await readStreamToJson(value as fs.ReadStream) }; + } else { + return { name, value: String(value) }; + } +} + function isJsonParsable(value: any) { if (typeof value !== 'string') return false; diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index b0071ffd02..fecf250801 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -15683,7 +15683,8 @@ export interface APIRequestContext { * 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. + * name, mime-type and its content. If the value is an array, each element will be sent as a separate field with the + * same name. */ multipart?: { [key: string]: string|number|boolean|ReadStream|{ /** @@ -15700,7 +15701,22 @@ export interface APIRequestContext { * File content */ buffer: Buffer; - }; }; + }|Array; }; /** * Query parameters to be sent with the URL. @@ -15817,7 +15833,8 @@ export interface APIRequestContext { * 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. + * name, mime-type and its content. If the value is an array, each element will be sent as a separate field with the + * same name. */ multipart?: { [key: string]: string|number|boolean|ReadStream|{ /** @@ -15834,7 +15851,22 @@ export interface APIRequestContext { * File content */ buffer: Buffer; - }; }; + }|Array; }; /** * Query parameters to be sent with the URL. @@ -15911,7 +15943,8 @@ export interface APIRequestContext { * 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. + * name, mime-type and its content. If the value is an array, each element will be sent as a separate field with the + * same name. */ multipart?: { [key: string]: string|number|boolean|ReadStream|{ /** @@ -15928,7 +15961,22 @@ export interface APIRequestContext { * File content */ buffer: Buffer; - }; }; + }|Array; }; /** * Query parameters to be sent with the URL. @@ -15991,7 +16039,8 @@ export interface APIRequestContext { * 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. + * name, mime-type and its content. If the value is an array, each element will be sent as a separate field with the + * same name. */ multipart?: { [key: string]: string|number|boolean|ReadStream|{ /** @@ -16008,7 +16057,22 @@ export interface APIRequestContext { * File content */ buffer: Buffer; - }; }; + }|Array; }; /** * Query parameters to be sent with the URL. @@ -16071,7 +16135,8 @@ export interface APIRequestContext { * 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. + * name, mime-type and its content. If the value is an array, each element will be sent as a separate field with the + * same name. */ multipart?: { [key: string]: string|number|boolean|ReadStream|{ /** @@ -16088,7 +16153,22 @@ export interface APIRequestContext { * File content */ buffer: Buffer; - }; }; + }|Array; }; /** * Query parameters to be sent with the URL. @@ -16202,7 +16282,8 @@ export interface APIRequestContext { * 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. + * name, mime-type and its content. If the value is an array, each element will be sent as a separate field with the + * same name. */ multipart?: { [key: string]: string|number|boolean|ReadStream|{ /** @@ -16219,7 +16300,22 @@ export interface APIRequestContext { * File content */ buffer: Buffer; - }; }; + }|Array; }; /** * Query parameters to be sent with the URL. @@ -16282,7 +16378,8 @@ export interface APIRequestContext { * 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. + * name, mime-type and its content. If the value is an array, each element will be sent as a separate field with the + * same name. */ multipart?: { [key: string]: string|number|boolean|ReadStream|{ /** @@ -16299,7 +16396,22 @@ export interface APIRequestContext { * File content */ buffer: Buffer; - }; }; + }|Array; }; /** * Query parameters to be sent with the URL. diff --git a/tests/library/browsercontext-fetch.spec.ts b/tests/library/browsercontext-fetch.spec.ts index bb0bb15362..7129803b2e 100644 --- a/tests/library/browsercontext-fetch.spec.ts +++ b/tests/library/browsercontext-fetch.spec.ts @@ -983,6 +983,40 @@ it('should support multipart/form-data and keep the order', async function({ con expect(response.status()).toBe(200); }); +it('should support repeating names in multipart/form-data', async function({ context, server }) { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/28070' }); + const postBodyPromise = new Promise(resolve => { + server.setRoute('/empty.html', async (req, res) => { + resolve((await req.postBody).toString('utf-8')); + res.writeHead(200, { + 'content-type': 'text/plain', + }); + res.end('OK.'); + }); + }); + const [postBody, response] = await Promise.all([ + postBodyPromise, + context.request.post(server.EMPTY_PAGE, { + multipart: { + firstName: 'John', + lastName: 'Doe', + file: [{ + name: 'f1.js', + mimeType: 'text/javascript', + buffer: Buffer.from('var x = 10;\r\n;console.log(x);') + }, { + name: 'f2.txt', + mimeType: 'text/plain', + buffer: Buffer.from('hello') + }] + } + }) + ]); + expect(postBody).toContain(`content-disposition: form-data; name="file"; filename="f1.js"\r\ncontent-type: text/javascript\r\n\r\nvar x = 10;\r\n;console.log(x);`); + expect(postBody).toContain(`content-disposition: form-data; name="file"; filename="f2.txt"\r\ncontent-type: text/plain\r\n\r\nhello`); + expect(response.status()).toBe(200); +}); + it('should serialize data to json regardless of content-type', async function({ context, server }) { const data = { firstName: 'John',