summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cli/js/web/fetch.ts45
-rw-r--r--cli/js/web/fetch/multipart.ts81
-rw-r--r--cli/tests/unit/fetch_test.ts84
3 files changed, 172 insertions, 38 deletions
diff --git a/cli/js/web/fetch.ts b/cli/js/web/fetch.ts
index 47ed1a7d1..045d1afcd 100644
--- a/cli/js/web/fetch.ts
+++ b/cli/js/web/fetch.ts
@@ -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")!;
- }
- }
- 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;
+ boundary = params.get("boundary")!;
}
- 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();
diff --git a/cli/js/web/fetch/multipart.ts b/cli/js/web/fetch/multipart.ts
index 792f9b5ee..654d4a0ea 100644
--- a/cli/js/web/fetch/multipart.ts
+++ b/cli/js/web/fetch/multipart.ts
@@ -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;
diff --git a/cli/tests/unit/fetch_test.ts b/cli/tests/unit/fetch_test.ts
index 5544eee24..98ff42737 100644
--- a/cli/tests/unit/fetch_test.ts
+++ b/cli/tests/unit/fetch_test.ts
@@ -268,6 +268,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
> {