mirror of
https://github.com/denoland/deno.git
synced 2025-01-08 15:19:40 -05:00
fix(cli/web/fetch): multipart/form-data request body support for binary files (#5886)
This commit is contained in:
parent
4feccdd3b7
commit
d907133944
3 changed files with 172 additions and 38 deletions
|
@ -2,15 +2,15 @@
|
||||||
import { notImplemented } from "../util.ts";
|
import { notImplemented } from "../util.ts";
|
||||||
import { isTypedArray } from "./util.ts";
|
import { isTypedArray } from "./util.ts";
|
||||||
import * as domTypes from "./dom_types.d.ts";
|
import * as domTypes from "./dom_types.d.ts";
|
||||||
import { TextDecoder, TextEncoder } from "./text_encoding.ts";
|
import { TextEncoder } from "./text_encoding.ts";
|
||||||
import { DenoBlob, bytesSymbol as blobBytesSymbol } from "./blob.ts";
|
import { DenoBlob, bytesSymbol as blobBytesSymbol } from "./blob.ts";
|
||||||
import { read } from "../ops/io.ts";
|
import { read } from "../ops/io.ts";
|
||||||
import { close } from "../ops/resources.ts";
|
import { close } from "../ops/resources.ts";
|
||||||
import { fetch as opFetch, FetchResponse } from "../ops/fetch.ts";
|
import { fetch as opFetch, FetchResponse } from "../ops/fetch.ts";
|
||||||
import * as Body from "./body.ts";
|
import * as Body from "./body.ts";
|
||||||
import { DomFileImpl } from "./dom_file.ts";
|
|
||||||
import { getHeaderValueParams } from "./util.ts";
|
import { getHeaderValueParams } from "./util.ts";
|
||||||
import { ReadableStreamImpl } from "./streams/readable_stream.ts";
|
import { ReadableStreamImpl } from "./streams/readable_stream.ts";
|
||||||
|
import { MultipartBuilder } from "./fetch/multipart.ts";
|
||||||
|
|
||||||
const NULL_BODY_STATUS = [101, 204, 205, 304];
|
const NULL_BODY_STATUS = [101, 204, 205, 304];
|
||||||
const REDIRECT_STATUS = [301, 302, 303, 307, 308];
|
const REDIRECT_STATUS = [301, 302, 303, 307, 308];
|
||||||
|
@ -232,45 +232,14 @@ export async function fetch(
|
||||||
body = init.body[blobBytesSymbol];
|
body = init.body[blobBytesSymbol];
|
||||||
contentType = init.body.type;
|
contentType = init.body.type;
|
||||||
} else if (init.body instanceof FormData) {
|
} else if (init.body instanceof FormData) {
|
||||||
let boundary = "";
|
let boundary;
|
||||||
if (headers.has("content-type")) {
|
if (headers.has("content-type")) {
|
||||||
const params = getHeaderValueParams("content-type");
|
const params = getHeaderValueParams("content-type");
|
||||||
if (params.has("boundary")) {
|
boundary = params.get("boundary")!;
|
||||||
boundary = params.get("boundary")!;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!boundary) {
|
const multipartBuilder = new MultipartBuilder(init.body, boundary);
|
||||||
boundary =
|
body = multipartBuilder.getBody();
|
||||||
"----------" +
|
contentType = multipartBuilder.getContentType();
|
||||||
Array.from(Array(32))
|
|
||||||
.map(() => Math.random().toString(36)[2] || 0)
|
|
||||||
.join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
let payload = "";
|
|
||||||
for (const [fieldName, fieldValue] of init.body.entries()) {
|
|
||||||
let part = `\r\n--${boundary}\r\n`;
|
|
||||||
part += `Content-Disposition: form-data; name=\"${fieldName}\"`;
|
|
||||||
if (fieldValue instanceof DomFileImpl) {
|
|
||||||
part += `; filename=\"${fieldValue.name}\"`;
|
|
||||||
}
|
|
||||||
part += "\r\n";
|
|
||||||
if (fieldValue instanceof DomFileImpl) {
|
|
||||||
part += `Content-Type: ${
|
|
||||||
fieldValue.type || "application/octet-stream"
|
|
||||||
}\r\n`;
|
|
||||||
}
|
|
||||||
part += "\r\n";
|
|
||||||
if (fieldValue instanceof DomFileImpl) {
|
|
||||||
part += new TextDecoder().decode(fieldValue[blobBytesSymbol]);
|
|
||||||
} else {
|
|
||||||
part += fieldValue;
|
|
||||||
}
|
|
||||||
payload += part;
|
|
||||||
}
|
|
||||||
payload += `\r\n--${boundary}--`;
|
|
||||||
body = new TextEncoder().encode(payload);
|
|
||||||
contentType = "multipart/form-data; boundary=" + boundary;
|
|
||||||
} else {
|
} else {
|
||||||
// TODO: ReadableStream
|
// TODO: ReadableStream
|
||||||
notImplemented();
|
notImplemented();
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
import { Buffer } from "../../buffer.ts";
|
||||||
|
import { bytesSymbol } from "../blob.ts";
|
||||||
|
import { DomFileImpl } from "../dom_file.ts";
|
||||||
import { DenoBlob } from "../blob.ts";
|
import { DenoBlob } from "../blob.ts";
|
||||||
import { TextEncoder, TextDecoder } from "../text_encoding.ts";
|
import { TextEncoder, TextDecoder } from "../text_encoding.ts";
|
||||||
import { getHeaderValueParams } from "../util.ts";
|
import { getHeaderValueParams } from "../util.ts";
|
||||||
|
@ -14,6 +17,84 @@ interface MultipartHeaders {
|
||||||
disposition: Map<string, string>;
|
disposition: Map<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class MultipartBuilder {
|
||||||
|
readonly boundary: string;
|
||||||
|
readonly formData: FormData;
|
||||||
|
readonly writer: Buffer;
|
||||||
|
constructor(formData: FormData, boundary?: string) {
|
||||||
|
this.boundary = boundary ?? this.#createBoundary();
|
||||||
|
this.formData = formData;
|
||||||
|
this.writer = new Buffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
getContentType(): string {
|
||||||
|
return `multipart/form-data; boundary=${this.boundary}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getBody(): Uint8Array {
|
||||||
|
for (const [fieldName, fieldValue] of this.formData.entries()) {
|
||||||
|
if (fieldValue instanceof DomFileImpl) {
|
||||||
|
this.#writeFile(fieldName, fieldValue);
|
||||||
|
} else this.#writeField(fieldName, fieldValue as string);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.writer.writeSync(encoder.encode(`\r\n--${this.boundary}--`));
|
||||||
|
|
||||||
|
return this.writer.bytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
#createBoundary = (): string => {
|
||||||
|
return (
|
||||||
|
"----------" +
|
||||||
|
Array.from(Array(32))
|
||||||
|
.map(() => Math.random().toString(36)[2] || 0)
|
||||||
|
.join("")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
#writeHeaders = (headers: string[][]): void => {
|
||||||
|
let buf = this.writer.empty() ? "" : "\r\n";
|
||||||
|
|
||||||
|
buf += `--${this.boundary}\r\n`;
|
||||||
|
for (const [key, value] of headers) {
|
||||||
|
buf += `${key}: ${value}\r\n`;
|
||||||
|
}
|
||||||
|
buf += `\r\n`;
|
||||||
|
|
||||||
|
this.writer.write(encoder.encode(buf));
|
||||||
|
};
|
||||||
|
|
||||||
|
#writeFileHeaders = (
|
||||||
|
field: string,
|
||||||
|
filename: string,
|
||||||
|
type?: string
|
||||||
|
): void => {
|
||||||
|
const headers = [
|
||||||
|
[
|
||||||
|
"Content-Disposition",
|
||||||
|
`form-data; name="${field}"; filename="${filename}"`,
|
||||||
|
],
|
||||||
|
["Content-Type", type || "application/octet-stream"],
|
||||||
|
];
|
||||||
|
return this.#writeHeaders(headers);
|
||||||
|
};
|
||||||
|
|
||||||
|
#writeFieldHeaders = (field: string): void => {
|
||||||
|
const headers = [["Content-Disposition", `form-data; name="${field}"`]];
|
||||||
|
return this.#writeHeaders(headers);
|
||||||
|
};
|
||||||
|
|
||||||
|
#writeField = (field: string, value: string): void => {
|
||||||
|
this.#writeFieldHeaders(field);
|
||||||
|
this.writer.writeSync(encoder.encode(value));
|
||||||
|
};
|
||||||
|
|
||||||
|
#writeFile = (field: string, value: DomFileImpl): void => {
|
||||||
|
this.#writeFileHeaders(field, value.name, value.type);
|
||||||
|
this.writer.writeSync(value[bytesSymbol]);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export class MultipartParser {
|
export class MultipartParser {
|
||||||
readonly boundary: string;
|
readonly boundary: string;
|
||||||
readonly boundaryChars: Uint8Array;
|
readonly boundaryChars: Uint8Array;
|
||||||
|
|
|
@ -267,6 +267,60 @@ unitTest(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
unitTest(
|
||||||
|
{ perms: { net: true } },
|
||||||
|
async function fetchInitFormDataMultipleFilesBody(): Promise<void> {
|
||||||
|
const files = [
|
||||||
|
{
|
||||||
|
// prettier-ignore
|
||||||
|
content: new Uint8Array([137,80,78,71,13,10,26,10, 137, 1, 25]),
|
||||||
|
type: "image/png",
|
||||||
|
name: "image",
|
||||||
|
fileName: "some-image.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// prettier-ignore
|
||||||
|
content: new Uint8Array([108,2,0,0,145,22,162,61,157,227,166,77,138,75,180,56,119,188,177,183]),
|
||||||
|
name: "file",
|
||||||
|
fileName: "file.bin",
|
||||||
|
expectedType: "application/octet-stream",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: new TextEncoder().encode("deno land"),
|
||||||
|
type: "text/plain",
|
||||||
|
name: "text",
|
||||||
|
fileName: "deno.txt",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const form = new FormData();
|
||||||
|
form.append("field", "value");
|
||||||
|
for (const file of files) {
|
||||||
|
form.append(
|
||||||
|
file.name,
|
||||||
|
new Blob([file.content], { type: file.type }),
|
||||||
|
file.fileName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const response = await fetch("http://localhost:4545/echo_server", {
|
||||||
|
method: "POST",
|
||||||
|
body: form,
|
||||||
|
});
|
||||||
|
const resultForm = await response.formData();
|
||||||
|
assertEquals(form.get("field"), resultForm.get("field"));
|
||||||
|
for (const file of files) {
|
||||||
|
const inputFile = form.get(file.name) as File;
|
||||||
|
const resultFile = resultForm.get(file.name) as File;
|
||||||
|
assertEquals(inputFile.size, resultFile.size);
|
||||||
|
assertEquals(inputFile.name, resultFile.name);
|
||||||
|
assertEquals(file.expectedType || file.type, resultFile.type);
|
||||||
|
assertEquals(
|
||||||
|
new Uint8Array(await resultFile.arrayBuffer()),
|
||||||
|
file.content
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
unitTest(
|
unitTest(
|
||||||
{
|
{
|
||||||
perms: { net: true },
|
perms: { net: true },
|
||||||
|
@ -427,6 +481,36 @@ unitTest(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
unitTest(
|
||||||
|
{ perms: { net: true } },
|
||||||
|
async function fetchInitFormDataTextFileBody(): Promise<void> {
|
||||||
|
const fileContent = "deno land";
|
||||||
|
const form = new FormData();
|
||||||
|
form.append("field", "value");
|
||||||
|
form.append(
|
||||||
|
"file",
|
||||||
|
new Blob([new TextEncoder().encode(fileContent)], {
|
||||||
|
type: "text/plain",
|
||||||
|
}),
|
||||||
|
"deno.txt"
|
||||||
|
);
|
||||||
|
const response = await fetch("http://localhost:4545/echo_server", {
|
||||||
|
method: "POST",
|
||||||
|
body: form,
|
||||||
|
});
|
||||||
|
const resultForm = await response.formData();
|
||||||
|
assertEquals(form.get("field"), resultForm.get("field"));
|
||||||
|
|
||||||
|
const file = form.get("file") as File;
|
||||||
|
const resultFile = resultForm.get("file") as File;
|
||||||
|
|
||||||
|
assertEquals(file.size, resultFile.size);
|
||||||
|
assertEquals(file.name, resultFile.name);
|
||||||
|
assertEquals(file.type, resultFile.type);
|
||||||
|
assertEquals(await file.text(), await resultFile.text());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
unitTest({ perms: { net: true } }, async function fetchUserAgent(): Promise<
|
unitTest({ perms: { net: true } }, async function fetchUserAgent(): Promise<
|
||||||
void
|
void
|
||||||
> {
|
> {
|
||||||
|
|
Loading…
Reference in a new issue