feat(fetch): multiple fields with the same name in post data (#30104)
Rerefence https://github.com/microsoft/playwright/issues/28070
This commit is contained in:
parent
911d8effb9
commit
a849ea9741
|
|
@ -566,7 +566,7 @@ api_request_context.post("https://example.com/api/createBook", data=data)
|
|||
|
||||
```csharp
|
||||
var data = new Dictionary<string, object>() {
|
||||
{ "firstNam", "John" },
|
||||
{ "firstName", "John" },
|
||||
{ "lastName", "Doe" }
|
||||
};
|
||||
await request.PostAsync("https://example.com/api/createBook", new() { DataObject = data });
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<string|number|boolean|fs.ReadStream|FilePayload>; };
|
||||
timeout?: number,
|
||||
failOnStatusCode?: boolean,
|
||||
ignoreHTTPSErrors?: boolean,
|
||||
|
|
@ -188,15 +188,11 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
|||
multipartData = [];
|
||||
// Convert file-like values to ServerFilePayload structs.
|
||||
for (const [name, value] of Object.entries(options.multipart)) {
|
||||
if (isFilePayload(value)) {
|
||||
const payload = value as FilePayload;
|
||||
if (!Buffer.isBuffer(payload.buffer))
|
||||
throw new Error(`Unexpected buffer type of 'data.${name}'`);
|
||||
multipartData.push({ name, file: filePayloadToJson(payload) });
|
||||
} else if (value instanceof fs.ReadStream) {
|
||||
multipartData.push({ name, file: await readStreamToJson(value as fs.ReadStream) });
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value)
|
||||
multipartData.push(await toFormField(name, item));
|
||||
} else {
|
||||
multipartData.push({ name, value: String(value) });
|
||||
multipartData.push(await toFormField(name, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -234,6 +230,19 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
|||
}
|
||||
}
|
||||
|
||||
async function toFormField(name: string, value: string|number|boolean|fs.ReadStream|FilePayload): Promise<channels.FormField> {
|
||||
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;
|
||||
|
|
|
|||
140
packages/playwright-core/types/types.d.ts
vendored
140
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -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<string|number|boolean|ReadStream|{
|
||||
/**
|
||||
* File name
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* File type
|
||||
*/
|
||||
mimeType: string;
|
||||
|
||||
/**
|
||||
* File content
|
||||
*/
|
||||
buffer: Buffer;
|
||||
}>; };
|
||||
|
||||
/**
|
||||
* 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<string|number|boolean|ReadStream|{
|
||||
/**
|
||||
* File name
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* File type
|
||||
*/
|
||||
mimeType: string;
|
||||
|
||||
/**
|
||||
* File content
|
||||
*/
|
||||
buffer: Buffer;
|
||||
}>; };
|
||||
|
||||
/**
|
||||
* 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<string|number|boolean|ReadStream|{
|
||||
/**
|
||||
* File name
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* File type
|
||||
*/
|
||||
mimeType: string;
|
||||
|
||||
/**
|
||||
* File content
|
||||
*/
|
||||
buffer: Buffer;
|
||||
}>; };
|
||||
|
||||
/**
|
||||
* 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<string|number|boolean|ReadStream|{
|
||||
/**
|
||||
* File name
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* File type
|
||||
*/
|
||||
mimeType: string;
|
||||
|
||||
/**
|
||||
* File content
|
||||
*/
|
||||
buffer: Buffer;
|
||||
}>; };
|
||||
|
||||
/**
|
||||
* 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<string|number|boolean|ReadStream|{
|
||||
/**
|
||||
* File name
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* File type
|
||||
*/
|
||||
mimeType: string;
|
||||
|
||||
/**
|
||||
* File content
|
||||
*/
|
||||
buffer: Buffer;
|
||||
}>; };
|
||||
|
||||
/**
|
||||
* 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<string|number|boolean|ReadStream|{
|
||||
/**
|
||||
* File name
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* File type
|
||||
*/
|
||||
mimeType: string;
|
||||
|
||||
/**
|
||||
* File content
|
||||
*/
|
||||
buffer: Buffer;
|
||||
}>; };
|
||||
|
||||
/**
|
||||
* 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<string|number|boolean|ReadStream|{
|
||||
/**
|
||||
* File name
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* File type
|
||||
*/
|
||||
mimeType: string;
|
||||
|
||||
/**
|
||||
* File content
|
||||
*/
|
||||
buffer: Buffer;
|
||||
}>; };
|
||||
|
||||
/**
|
||||
* Query parameters to be sent with the URL.
|
||||
|
|
|
|||
|
|
@ -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<string>(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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue