mirror of
https://github.com/microsoft/playwright.git
synced 2024-09-11 20:37:54 +03:00
feat(fetch): support FormData as multipart parameter (#30489)
Reference https://github.com/microsoft/playwright/issues/28070
This commit is contained in:
parent
73c12f1f77
commit
230a8437d3
@ -159,7 +159,10 @@ context cookies from the response. The method will automatically follow redirect
|
||||
### option: APIRequestContext.delete.form = %%-csharp-fetch-option-form-%%
|
||||
* since: v1.17
|
||||
|
||||
### option: APIRequestContext.delete.multipart = %%-js-python-fetch-option-multipart-%%
|
||||
### option: APIRequestContext.delete.multipart = %%-js-fetch-option-multipart-%%
|
||||
* since: v1.17
|
||||
|
||||
### option: APIRequestContext.delete.multipart = %%-python-fetch-option-multipart-%%
|
||||
* since: v1.17
|
||||
|
||||
### option: APIRequestContext.delete.multipart = %%-csharp-fetch-option-multipart-%%
|
||||
@ -324,7 +327,10 @@ If set changes the fetch method (e.g. [PUT](https://developer.mozilla.org/en-US/
|
||||
### option: APIRequestContext.fetch.form = %%-csharp-fetch-option-form-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.fetch.multipart = %%-js-python-fetch-option-multipart-%%
|
||||
### option: APIRequestContext.fetch.multipart = %%-js-fetch-option-multipart-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.fetch.multipart = %%-python-fetch-option-multipart-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.fetch.multipart = %%-csharp-fetch-option-multipart-%%
|
||||
@ -410,7 +416,10 @@ await request.GetAsync("https://example.com/api/getText", new() { Params = query
|
||||
### option: APIRequestContext.get.form = %%-csharp-fetch-option-form-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.get.multipart = %%-js-python-fetch-option-multipart-%%
|
||||
### option: APIRequestContext.get.multipart = %%-js-fetch-option-multipart-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.get.multipart = %%-python-fetch-option-multipart-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.get.multipart = %%-csharp-fetch-option-multipart-%%
|
||||
@ -460,7 +469,10 @@ context cookies from the response. The method will automatically follow redirect
|
||||
### option: APIRequestContext.head.form = %%-csharp-fetch-option-form-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.head.multipart = %%-js-python-fetch-option-multipart-%%
|
||||
### option: APIRequestContext.head.multipart = %%-js-fetch-option-multipart-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.head.multipart = %%-python-fetch-option-multipart-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.head.multipart = %%-csharp-fetch-option-multipart-%%
|
||||
@ -510,7 +522,10 @@ context cookies from the response. The method will automatically follow redirect
|
||||
### option: APIRequestContext.patch.form = %%-csharp-fetch-option-form-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.patch.multipart = %%-js-python-fetch-option-multipart-%%
|
||||
### option: APIRequestContext.patch.multipart = %%-js-fetch-option-multipart-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.patch.multipart = %%-python-fetch-option-multipart-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.patch.multipart = %%-csharp-fetch-option-multipart-%%
|
||||
@ -566,7 +581,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 });
|
||||
@ -690,7 +705,10 @@ await request.PostAsync("https://example.com/api/uploadScript", new() { Multipar
|
||||
### option: APIRequestContext.post.form = %%-csharp-fetch-option-form-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.post.multipart = %%-js-python-fetch-option-multipart-%%
|
||||
### option: APIRequestContext.post.multipart = %%-js-fetch-option-multipart-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.post.multipart = %%-python-fetch-option-multipart-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.post.multipart = %%-csharp-fetch-option-multipart-%%
|
||||
@ -740,7 +758,10 @@ context cookies from the response. The method will automatically follow redirect
|
||||
### option: APIRequestContext.put.form = %%-csharp-fetch-option-form-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.put.multipart = %%-js-python-fetch-option-multipart-%%
|
||||
### option: APIRequestContext.put.multipart = %%-js-fetch-option-multipart-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.put.multipart = %%-python-fetch-option-multipart-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.put.multipart = %%-csharp-fetch-option-multipart-%%
|
||||
|
@ -14,6 +14,77 @@ FormData form = FormData.create()
|
||||
page.request().post("http://localhost/submit", RequestOptions.create().setForm(form));
|
||||
```
|
||||
|
||||
## method: FormData.append
|
||||
* since: v1.44
|
||||
- returns: <[FormData]>
|
||||
|
||||
Appends a new value onto an existing key inside a FormData object, or adds the key if it
|
||||
does not already exist. File values can be passed either as `Path` or as `FilePayload`.
|
||||
Multiple fields with the same name can be added.
|
||||
|
||||
The difference between [`method: FormData.set`] and [`method: FormData.append`] is that if the specified key already exists,
|
||||
[`method: FormData.set`] will overwrite all existing values with the new one, whereas [`method: FormData.append`] will append
|
||||
the new value onto the end of the existing set of values.
|
||||
|
||||
```java
|
||||
import com.microsoft.playwright.options.FormData;
|
||||
...
|
||||
FormData form = FormData.create()
|
||||
// Only name and value are set.
|
||||
.append("firstName", "John")
|
||||
// Name and value are set, filename and Content-Type are inferred from the file path.
|
||||
.append("attachment", Paths.get("pic.jpg"))
|
||||
// Name, value, filename and Content-Type are set.
|
||||
.append("attachment", new FilePayload("table.csv", "text/csv", Files.readAllBytes(Paths.get("my-tble.csv"))));
|
||||
page.request().post("http://localhost/submit", RequestOptions.create().setForm(form));
|
||||
```
|
||||
|
||||
```csharp
|
||||
var multipart = Context.APIRequest.CreateFormData();
|
||||
// Only name and value are set.
|
||||
multipart.Append("firstName", "John");
|
||||
// Name, value, filename and Content-Type are set.
|
||||
multipart.Append("attachment", new FilePayload()
|
||||
{
|
||||
Name = "pic.jpg",
|
||||
MimeType = "image/jpeg",
|
||||
Buffer = File.ReadAllBytes("john.jpg")
|
||||
});
|
||||
// Name, value, filename and Content-Type are set.
|
||||
multipart.Append("attachment", new FilePayload()
|
||||
{
|
||||
Name = "table.csv",
|
||||
MimeType = "text/csv",
|
||||
Buffer = File.ReadAllBytes("my-tble.csv")
|
||||
});
|
||||
await Page.APIRequest.PostAsync("https://localhost/submit", new() { Multipart = multipart });
|
||||
```
|
||||
|
||||
### param: FormData.append.name
|
||||
* since: v1.44
|
||||
- `name` <[string]>
|
||||
|
||||
Field name.
|
||||
|
||||
### param: FormData.append.value
|
||||
* since: v1.44
|
||||
- `value` <[string]|[boolean]|[int]|[Path]|[Object]>
|
||||
- `name` <[string]> File name
|
||||
- `mimeType` <[string]> File type
|
||||
- `buffer` <[Buffer]> File content
|
||||
|
||||
Field value.
|
||||
|
||||
### param: FormData.append.value
|
||||
* since: v1.44
|
||||
* langs: csharp
|
||||
- `value` <[string]|[boolean]|[int]|[Object]>
|
||||
- `name` <[string]> File name
|
||||
- `mimeType` <[string]> File type
|
||||
- `buffer` <[Buffer]> File content
|
||||
|
||||
Field value.
|
||||
|
||||
## method: FormData.create
|
||||
* since: v1.18
|
||||
* langs: java
|
||||
@ -36,7 +107,7 @@ FormData form = FormData.create()
|
||||
// Name and value are set, filename and Content-Type are inferred from the file path.
|
||||
.set("profilePicture1", Paths.get("john.jpg"))
|
||||
// Name, value, filename and Content-Type are set.
|
||||
.set("profilePicture2", new FilePayload("john.jpg", "image/jpeg", Files.readAllBytes(Paths.get("john.jpg"))));
|
||||
.set("profilePicture2", new FilePayload("john.jpg", "image/jpeg", Files.readAllBytes(Paths.get("john.jpg"))))
|
||||
.set("age", 30);
|
||||
page.request().post("http://localhost/submit", RequestOptions.create().setForm(form));
|
||||
```
|
||||
@ -52,6 +123,7 @@ multipart.Set("profilePicture", new FilePayload()
|
||||
MimeType = "image/jpeg",
|
||||
Buffer = File.ReadAllBytes("john.jpg")
|
||||
});
|
||||
multipart.Set("age", 30);
|
||||
await Page.APIRequest.PostAsync("https://localhost/submit", new() { Multipart = multipart });
|
||||
```
|
||||
|
||||
|
@ -403,9 +403,9 @@ unless explicitly provided.
|
||||
|
||||
An instance of [FormData] can be created via [`method: APIRequestContext.createFormData`].
|
||||
|
||||
## js-python-fetch-option-multipart
|
||||
* langs: js, python
|
||||
- `multipart` <[Object]<[string], [string]|[float]|[boolean]|[ReadStream]|[Object]>>
|
||||
## js-fetch-option-multipart
|
||||
* langs: js
|
||||
- `multipart` <[FormData]|[Object]<[string], [string]|[float]|[boolean]|[ReadStream]|[Object]>>
|
||||
- `name` <[string]> File name
|
||||
- `mimeType` <[string]> File type
|
||||
- `buffer` <[Buffer]> File content
|
||||
@ -415,14 +415,24 @@ this request body. If this parameter is specified `content-type` header will be
|
||||
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.
|
||||
|
||||
## python-fetch-option-multipart
|
||||
* langs: python
|
||||
- `multipart` <[Object]<[string], [string]|[float]|[boolean]|[ReadStream]|[Object]>>
|
||||
- `name` <[string]> File name
|
||||
- `mimeType` <[string]> File type
|
||||
- `buffer` <[Buffer]> File content
|
||||
|
||||
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 as file-like object containing file name, mime-type and its content.
|
||||
|
||||
## csharp-fetch-option-multipart
|
||||
* langs: csharp
|
||||
- `multipart` <[FormData]>
|
||||
|
||||
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.
|
||||
unless explicitly provided. File values can be passed as file-like object containing file name, mime-type and its content.
|
||||
|
||||
An instance of [FormData] can be created via [`method: APIRequestContext.createFormData`].
|
||||
|
||||
|
@ -187,18 +187,24 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
||||
formData = objectToArray(options.form);
|
||||
} else if (options.multipart) {
|
||||
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) });
|
||||
} else {
|
||||
multipartData.push({ name, value: String(value) });
|
||||
if (globalThis.FormData && options.multipart instanceof FormData) {
|
||||
const form = options.multipart;
|
||||
for (const [name, value] of form.entries()) {
|
||||
if (isString(value)) {
|
||||
multipartData.push({ name, value });
|
||||
} else {
|
||||
const file: ServerFilePayload = {
|
||||
name: value.name,
|
||||
mimeType: value.type,
|
||||
buffer: Buffer.from(await value.arrayBuffer()),
|
||||
};
|
||||
multipartData.push({ name, file });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Convert file-like values to ServerFilePayload structs.
|
||||
for (const [name, value] of Object.entries(options.multipart))
|
||||
multipartData.push(await toFormField(name, value));
|
||||
}
|
||||
}
|
||||
if (postDataBuffer === undefined && jsonData === undefined && formData === undefined && multipartData === undefined)
|
||||
@ -235,6 +241,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;
|
||||
|
14
packages/playwright-core/types/types.d.ts
vendored
14
packages/playwright-core/types/types.d.ts
vendored
@ -15742,7 +15742,7 @@ export interface APIRequestContext {
|
||||
* [`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.
|
||||
*/
|
||||
multipart?: { [key: string]: string|number|boolean|ReadStream|{
|
||||
multipart?: FormData|{ [key: string]: string|number|boolean|ReadStream|{
|
||||
/**
|
||||
* File name
|
||||
*/
|
||||
@ -15876,7 +15876,7 @@ export interface APIRequestContext {
|
||||
* [`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.
|
||||
*/
|
||||
multipart?: { [key: string]: string|number|boolean|ReadStream|{
|
||||
multipart?: FormData|{ [key: string]: string|number|boolean|ReadStream|{
|
||||
/**
|
||||
* File name
|
||||
*/
|
||||
@ -15970,7 +15970,7 @@ export interface APIRequestContext {
|
||||
* [`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.
|
||||
*/
|
||||
multipart?: { [key: string]: string|number|boolean|ReadStream|{
|
||||
multipart?: FormData|{ [key: string]: string|number|boolean|ReadStream|{
|
||||
/**
|
||||
* File name
|
||||
*/
|
||||
@ -16050,7 +16050,7 @@ export interface APIRequestContext {
|
||||
* [`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.
|
||||
*/
|
||||
multipart?: { [key: string]: string|number|boolean|ReadStream|{
|
||||
multipart?: FormData|{ [key: string]: string|number|boolean|ReadStream|{
|
||||
/**
|
||||
* File name
|
||||
*/
|
||||
@ -16130,7 +16130,7 @@ export interface APIRequestContext {
|
||||
* [`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.
|
||||
*/
|
||||
multipart?: { [key: string]: string|number|boolean|ReadStream|{
|
||||
multipart?: FormData|{ [key: string]: string|number|boolean|ReadStream|{
|
||||
/**
|
||||
* File name
|
||||
*/
|
||||
@ -16261,7 +16261,7 @@ export interface APIRequestContext {
|
||||
* [`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.
|
||||
*/
|
||||
multipart?: { [key: string]: string|number|boolean|ReadStream|{
|
||||
multipart?: FormData|{ [key: string]: string|number|boolean|ReadStream|{
|
||||
/**
|
||||
* File name
|
||||
*/
|
||||
@ -16341,7 +16341,7 @@ export interface APIRequestContext {
|
||||
* [`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.
|
||||
*/
|
||||
multipart?: { [key: string]: string|number|boolean|ReadStream|{
|
||||
multipart?: FormData|{ [key: string]: string|number|boolean|ReadStream|{
|
||||
/**
|
||||
* File name
|
||||
*/
|
||||
|
@ -23,6 +23,7 @@ import zlib from 'zlib';
|
||||
import { contextTest as it, expect } from '../config/browserTest';
|
||||
import { suppressCertificateWarning } from '../config/utils';
|
||||
import { kTargetClosedErrorMessage } from 'tests/config/errors';
|
||||
import * as buffer from 'buffer';
|
||||
|
||||
it.skip(({ mode }) => mode !== 'default');
|
||||
|
||||
@ -983,6 +984,39 @@ 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 nodeVersion = +process.versions.node.split('.')[0];
|
||||
it.skip(nodeVersion < 18, 'FormData is not available in Node.js < 18');
|
||||
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 formData = new FormData();
|
||||
formData.set('name', 'John');
|
||||
formData.append('name', 'Doe');
|
||||
formData.append('file', new (buffer as any).File(['var x = 10;\r\n;console.log(x);'], 'f1.js', { type: 'text/javascript' }));
|
||||
formData.append('file', new (buffer as any).File(['hello'], 'f2.txt', { type: 'text/plain' }), 'custom_f2.txt');
|
||||
formData.append('file', new (buffer as any).Blob(['boo'], { type: 'text/plain' }));
|
||||
const [postBody, response] = await Promise.all([
|
||||
postBodyPromise,
|
||||
context.request.post(server.EMPTY_PAGE, {
|
||||
multipart: formData
|
||||
})
|
||||
]);
|
||||
expect(postBody).toContain(`content-disposition: form-data; name="name"\r\n\r\nJohn`);
|
||||
expect(postBody).toContain(`content-disposition: form-data; name="name"\r\n\r\nDoe`);
|
||||
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="custom_f2.txt"\r\ncontent-type: text/plain\r\n\r\nhello`);
|
||||
expect(postBody).toContain(`content-disposition: form-data; name="file"; filename="blob"\r\ncontent-type: text/plain\r\n\r\nboo`);
|
||||
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 New Issue
Block a user