1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-22 07:14:47 -05:00

fix(cli/web/fetch): multipart/form-data request body support for binary files (#5886)

This commit is contained in:
Marcos Casagrande 2020-06-08 18:08:26 +02:00 committed by GitHub
parent 4feccdd3b7
commit d907133944
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 172 additions and 38 deletions

View file

@ -2,15 +2,15 @@
import { notImplemented } from "../util.ts";
import { isTypedArray } from "./util.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 { read } from "../ops/io.ts";
import { close } from "../ops/resources.ts";
import { fetch as opFetch, FetchResponse } from "../ops/fetch.ts";
import * as Body from "./body.ts";
import { DomFileImpl } from "./dom_file.ts";
import { getHeaderValueParams } from "./util.ts";
import { ReadableStreamImpl } from "./streams/readable_stream.ts";
import { MultipartBuilder } from "./fetch/multipart.ts";
const NULL_BODY_STATUS = [101, 204, 205, 304];
const REDIRECT_STATUS = [301, 302, 303, 307, 308];
@ -232,45 +232,14 @@ export async function fetch(
body = init.body[blobBytesSymbol];
contentType = init.body.type;
} else if (init.body instanceof FormData) {
let boundary = "";
let boundary;
if (headers.has("content-type")) {
const params = getHeaderValueParams("content-type");
if (params.has("boundary")) {
boundary = params.get("boundary")!;
}
boundary = params.get("boundary")!;
}
if (!boundary) {
boundary =
"----------" +
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;
const multipartBuilder = new MultipartBuilder(init.body, boundary);
body = multipartBuilder.getBody();
contentType = multipartBuilder.getContentType();
} else {
// TODO: ReadableStream
notImplemented();

View file

@ -1,5 +1,8 @@
// 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 { TextEncoder, TextDecoder } from "../text_encoding.ts";
import { getHeaderValueParams } from "../util.ts";
@ -14,6 +17,84 @@ interface MultipartHeaders {
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 {
readonly boundary: string;
readonly boundaryChars: Uint8Array;

View file

@ -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(
{
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<
void
> {