1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372 |
x7
x7
x7
x7
x19
x19
x7
x7
x19
x19
x19
x19
x19
x19
x19
x19
x19
x19
x19
x19
x31
x19
x19
x188
x188
x188
x188
x188
x188
x188
x188
x197
x197
x197
x188
x216
x188
x444
x319
x325
x325
x325
x325
x188
x189
x189
x19
x19
x21
x21
x19
x19
x7
x7
x19
x19
x19
x19
x19
x19
x19
x19
x19
x19
x19
x34
x34
x34
x34
x45
x45
x45
x34
x34
x41
x41
x34
x45
x42
x48
x48
x47
x51
x51
x34
x38
x34
x45
x135
x45
x45
x74
x222
x102
x101
x74
x45
x47
x48
x48
x45
x47
x47
x112
x28
x19
x7
x100
x100
x100
x100
x7
x45
x45
x45
x7
x11
x44
x11
x11
x7
x9
x36
x9
x9
x9
x7
x17
x17
x7
x10
x10
x7
x13
x13
x7 |
|
// Copyright 2018-2026 the Deno authors. MIT license.
import { encodeBase64 } from "@std/encoding/unstable-base64";
import { toByteStream } from "@std/streams/unstable-to-byte-stream";
/**
* The input that can be passed to a {@linkcode FormDataEncoderStream}.
*
* @experimental **UNSTABLE**: New API, yet to be vetted.
*/
export interface FormDataInput {
/**
* A string containing the key used for the form data entry. While all UTF-8
* characters should work, it is encouraged to only use ascii. The value of
* `name` should not generally be exposed to the user.
*/
name: string;
/**
* The value of the form data entry.
*/
value: string | Blob | ReadableStream<Uint8Array>;
/**
* The content type of the form data entry.
* Defaults to:
* - `text/plain` when a string is passed to `value`.
* - `application/octet-stream` when a {@linkcode Blob} is passed to `value`
* and the {@linkcode Blob} doesn't have its own type.
* - `application/octet-stream` when a {@linkcode ReadableStream<Uint8Array>}
* is passed to `value`.
*/
contentType?: string;
/**
* The filename of the form data entry.
* Defaults to "blob" when a {@linkcode Blob} or
* {@linkcode ReadableStream<Uint8Array>} is passed to `value`.
*/
filename?: string;
}
/**
* ### Overview
* {@linkcode FormDataEncoderStream} is a class based off the
* [RFC 7578](https://datatracker.ietf.org/doc/html/rfc7578) spec and offers a
* way to create a {@linkcode FormData} in a streaming manner. Enabling one to
* send large amounts of information without having it all locally in memory
* first.
*
* ### Limitations
* Due to the structure of the `multipart` media type and the constraints of a
* streaming implementation, it is not possible to guarantee that a chosen
* boundary string will never occur within the payload itself. As a result, the
* appearance of a boundary sequence within the content must be considered a
* valid and unavoidable edge case inherent to this API. Each instance of this
* class will result in a different boundary being generated.
*
* @experimental **UNSTABLE**: New API, yet to be vetted.
*
* @example Usage
* ```ts
* import { assert, assertEquals } from "@std/assert";
* import { FormDataEncoderStream } from "@std/http/unstable-formdata-encoder-stream";
*
* const response = FormDataEncoderStream.from(ReadableStream.from([
* {
* name: "file",
* value: (await Deno.open("deno.json")).readable,
* contentType: "application/json",
* }
* ]))
* .toResponse();
*
* const formData = await response.formData();
* const file = formData.get("file");
* assert(typeof file !== "string");
* assertEquals(await file?.text(), await Deno.readTextFile("deno.json"));
* ```
*/
export class FormDataEncoderStream {
#encoder = new TextEncoder();
#readable: ReadableStream<Uint8Array>;
#boundary: Uint8Array;
#contentType: string;
/**
* Constructs a new instance.
*
* @param readable The readable stream of form data inputs.
*/
constructor(readable: ReadableStream<FormDataInput>) {
const boundary = "--deno-std-" +
encodeBase64(crypto.getRandomValues(new Uint8Array(30))) +
"\r\n";
this.#encoder
.encodeInto(boundary, this.#boundary = new Uint8Array(boundary.length));
this.#contentType = 'multipart/form-data; boundary="' +
boundary.slice(2, -2) + '"';
const gen = this.#handle(readable);
this.#readable = new ReadableStream({
type: "bytes",
autoAllocateChunkSize: 1024,
async start() {
await gen.next();
},
async pull(controller) {
const length = controller.byobRequest!.view!.byteLength;
const buffer = new Uint8Array(
controller.byobRequest!.view!.buffer,
controller.byobRequest!.view!.byteOffset,
length,
);
try {
const { done, value } = await gen.next(buffer);
if (done) {
controller.close();
return controller.byobRequest!.respond(0);
}
// deno-lint-ignore no-explicit-any
if ((buffer.buffer as any).detached) {
controller.byobRequest!.respondWithNewView(value);
} else if (buffer.buffer === value.buffer) {
controller.byobRequest!.respond(value.length);
} else {
buffer.set(value.subarray(0, length));
controller.byobRequest!.respond(length);
controller.enqueue(value.subarray(length));
}
} catch (error) {
controller.error(error);
}
},
async cancel(reason) {
await gen.throw(reason).catch(() => undefined);
},
});
}
async *#handle(
readable: ReadableStream<FormDataInput>,
): AsyncGenerator<Uint8Array, void, Uint8Array> {
const fixed = [
this.#encoder.encode('Content-Disposition: form-data; name="'),
this.#encoder.encode('"; filename="'),
this.#encoder.encode('"\r\nContent-Type: '),
this.#encoder.encode('text/plain; charset="UTF-8"\r\n\r\n'),
this.#encoder.encode("application/octet-stream\r\n\r\n"),
this.#encoder.encode("\r\n\r\n"),
this.#encoder.encode("\r\n"),
];
let buffer = yield new Uint8Array();
for await (const input of readable) {
buffer = yield this.#setBuffer(buffer, this.#boundary);
buffer = yield this.#setBuffer(buffer, fixed[0]!);
buffer = yield this.#setString(buffer, input.name);
if (input.filename != undefined || typeof input.value !== "string") {
buffer = yield this.#setBuffer(buffer, fixed[1]!);
buffer = yield this.#setString(buffer, input.filename ?? "blob");
}
buffer = yield this.#setBuffer(buffer, fixed[2]!);
if (input.contentType != undefined) {
buffer = yield this.#setString(buffer, input.contentType);
buffer = yield this.#setBuffer(buffer, fixed[5]!);
} else if (typeof input.value === "string") {
buffer = yield this.#setBuffer(buffer, fixed[3]!);
} else if (input.value instanceof Blob && input.value.type.length) {
buffer = yield this.#setString(buffer, input.value.type);
buffer = yield this.#setBuffer(buffer, fixed[5]!);
} else {
buffer = yield this.#setBuffer(buffer, fixed[4]!);
}
if (typeof input.value === "string") {
buffer = yield this.#setString(buffer, input.value);
} else {
if (input.value instanceof Blob) input.value = input.value.stream();
const reader = toByteStream(input.value).getReader({ mode: "byob" });
try {
while (true) {
const { done, value } = await reader
.read(buffer, { min: buffer.length });
buffer = yield value!;
if (done) break;
}
} catch (reason) {
await reader.cancel(reason);
throw reason;
}
}
buffer = yield this.#setBuffer(buffer, fixed[6]!);
}
this.#boundary.set([45, 45], this.#boundary.length - 2);
yield this.#setBuffer(buffer, this.#boundary);
}
#setBuffer(buffer: Uint8Array, x: Uint8Array): Uint8Array {
if (buffer.length < x.length) buffer = new Uint8Array(x.length);
buffer.set(x);
return buffer.subarray(0, x.length);
}
#setString(buffer: Uint8Array, x: string): Uint8Array {
if (buffer.length < x.length * 3) buffer = new Uint8Array(x.length * 3);
return buffer.subarray(0, this.#encoder.encodeInto(x, buffer).written);
}
/**
* Convert the {@linkcode FormDataEncoderStream} to a {@linkcode Response} to send to a
* client.
*
* @param init Optional response initialization options.
* @returns The {@linkcode Response} containing the encoded form data.
*
* @example Usage
* ```ts
* import { assert, assertEquals } from "@std/assert";
* import { FormDataEncoderStream } from "@std/http/unstable-formdata-encoder-stream";
*
* const response = FormDataEncoderStream.from(ReadableStream.from([
* {
* name: "file",
* value: (await Deno.open("deno.json")).readable,
* contentType: "application/json",
* }
* ]))
* .toResponse();
*
* const formData = await response.formData();
* const file = formData.get("file");
* assert(typeof file !== "string");
* assertEquals(await file?.text(), await Deno.readTextFile("deno.json"));
* ```
*/
toResponse(init?: ResponseInit): Response {
init ??= {};
init.headers = { ...init.headers, "Content-Type": this.#contentType };
return new Response(this.#readable, init);
}
/**
* Convert the {@linkcode FormDataEncoderStream} to a {@linkcode Request} to send to a server.
*
* @param input The URL or RequestInfo to send the request to.
* @param init Optional request initialization options.
* @returns The {@linkcode Request} containing the encoded form data.
*
* @example Usage
* ```ts
* import { assert, assertEquals } from "@std/assert";
* import { FormDataEncoderStream } from "@std/http/unstable-formdata-encoder-stream";
*
* const request = FormDataEncoderStream.from(ReadableStream.from([
* {
* name: "file",
* value: (await Deno.open("deno.json")).readable,
* contentType: "application/json",
* }
* ]))
* .toRequest("https://example.com", { method: "POST" });
*
* const formData = await request.formData();
* const file = formData.get("file");
* assert(typeof file !== "string");
* assertEquals(await file?.text(), await Deno.readTextFile("deno.json"));
* ```
*/
toRequest(input: RequestInfo | URL, init?: RequestInit): Request {
init ??= {};
init.headers = { ...init.headers, "Content-Type": this.#contentType };
init.body = this.#readable;
return new Request(input, init);
}
/**
* Create a {@linkcode FormDataEncoderStream} from a
* {@linkcode ReadableStream}.
*
* @param readable The `ReadableStream` to encode.
* @returns The {@linkcode FormDataEncoderStream} containing the encoded form
* data.
*
* @example Usage
* ```ts
* import { assert, assertEquals } from "@std/assert";
* import { FormDataEncoderStream } from "@std/http/unstable-formdata-encoder-stream";
*
* const response = FormDataEncoderStream.from(ReadableStream.from([
* {
* name: "file",
* value: (await Deno.open("deno.json")).readable,
* contentType: "application/json",
* }
* ]))
* .toResponse();
*
* const formData = await response.formData();
* const file = formData.get("file");
* assert(typeof file !== "string");
* assertEquals(await file?.text(), await Deno.readTextFile("deno.json"));
* ```
*/
static from(readable: ReadableStream<FormDataInput>): FormDataEncoderStream {
return new FormDataEncoderStream(readable);
}
/**
* The content type of the `ReadableStream`. Contains the boundary
* required for decoding.
*
* @returns The content type of the `ReadableStream`.
*
* @example Usage
* ```ts
* import { assert, assertEquals } from "@std/assert";
* import { FormDataEncoderStream } from "@std/http/unstable-formdata-encoder-stream";
*
* const encoder = FormDataEncoderStream.from(ReadableStream.from([
* {
* name: "file",
* value: (await Deno.open("deno.json")).readable,
* contentType: "application/json",
* }
* ]));
*
* const response = new Response(encoder.readable, {
* headers: { "Content-Type": encoder.contentType },
* });
*
* const formData = await response.formData();
* const file = formData.get("file");
* assert(typeof file !== "string");
* assertEquals(await file?.text(), await Deno.readTextFile("deno.json"));
* ```
*/
get contentType(): string {
return this.#contentType;
}
/**
* The ReadableStream containing the encoded content.
*
* @returns The `ReadableStream` containing the encoded form data.
*
* @example Usage
* ```ts
* import { assert, assertEquals } from "@std/assert";
* import { FormDataEncoderStream } from "@std/http/unstable-formdata-encoder-stream";
*
* const encoder = FormDataEncoderStream.from(ReadableStream.from([
* {
* name: "file",
* value: (await Deno.open("deno.json")).readable,
* contentType: "application/json",
* }
* ]));
*
* const response = new Response(encoder.readable, {
* headers: { "Content-Type": encoder.contentType },
* });
*
* const formData = await response.formData();
* const file = formData.get("file");
* assert(typeof file !== "string");
* assertEquals(await file?.text(), await Deno.readTextFile("deno.json"));
* ```
*/
get readable(): ReadableStream<Uint8Array> {
return this.#readable;
}
}
|