@npy/fetch 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/README.md +143 -50
  2. package/bun.lock +68 -0
  3. package/examples/custom-proxy-client.ts +32 -0
  4. package/examples/http-client.ts +47 -0
  5. package/examples/proxy.ts +16 -0
  6. package/examples/simple.ts +15 -0
  7. package/package.json +36 -25
  8. package/src/_internal/consts.ts +3 -0
  9. package/src/_internal/decode-stream-error.ts +16 -0
  10. package/src/_internal/error-mapping.ts +160 -0
  11. package/src/_internal/guards.ts +78 -0
  12. package/src/_internal/net.ts +173 -0
  13. package/src/_internal/promises.ts +22 -0
  14. package/src/_internal/streams.ts +52 -0
  15. package/src/_internal/symbols.ts +1 -0
  16. package/src/agent-pool.ts +157 -0
  17. package/src/agent.ts +408 -0
  18. package/src/body.ts +179 -0
  19. package/src/dialers/index.ts +3 -0
  20. package/src/dialers/proxy.ts +102 -0
  21. package/src/dialers/tcp.ts +162 -0
  22. package/src/encoding.ts +222 -0
  23. package/src/errors.ts +357 -0
  24. package/src/fetch.ts +626 -0
  25. package/src/http-client.ts +111 -0
  26. package/src/index.ts +14 -0
  27. package/src/io/_utils.ts +82 -0
  28. package/src/io/buf-writer.ts +183 -0
  29. package/src/io/io.ts +322 -0
  30. package/src/io/readers.ts +576 -0
  31. package/src/io/writers.ts +331 -0
  32. package/src/types/agent.ts +98 -0
  33. package/src/types/{dialer.d.cts → dialer.ts} +22 -9
  34. package/src/types/index.ts +2 -0
  35. package/tests/agent-pool.test.ts +111 -0
  36. package/tests/agent.test.ts +134 -0
  37. package/tests/body.test.ts +228 -0
  38. package/tests/errors.test.ts +152 -0
  39. package/tests/fetch.test.ts +421 -0
  40. package/tests/io-options.test.ts +127 -0
  41. package/tests/multipart.test.ts +348 -0
  42. package/tests/test-utils.ts +335 -0
  43. package/tsconfig.json +15 -0
  44. package/LICENSE +0 -21
  45. package/_internal/consts.cjs +0 -4
  46. package/_internal/consts.js +0 -4
  47. package/_internal/error-adapters.cjs +0 -146
  48. package/_internal/error-adapters.js +0 -142
  49. package/_internal/guards.cjs +0 -24
  50. package/_internal/guards.js +0 -17
  51. package/_internal/net.cjs +0 -95
  52. package/_internal/net.js +0 -92
  53. package/_internal/promises.cjs +0 -18
  54. package/_internal/promises.js +0 -18
  55. package/_internal/streams.cjs +0 -37
  56. package/_internal/streams.js +0 -36
  57. package/_virtual/_rolldown/runtime.cjs +0 -23
  58. package/agent-pool.cjs +0 -78
  59. package/agent-pool.js +0 -77
  60. package/agent.cjs +0 -257
  61. package/agent.js +0 -256
  62. package/body.cjs +0 -154
  63. package/body.js +0 -151
  64. package/dialers/proxy.cjs +0 -49
  65. package/dialers/proxy.js +0 -48
  66. package/dialers/tcp.cjs +0 -70
  67. package/dialers/tcp.js +0 -67
  68. package/encoding.cjs +0 -95
  69. package/encoding.js +0 -91
  70. package/errors.cjs +0 -275
  71. package/errors.js +0 -259
  72. package/fetch.cjs +0 -117
  73. package/fetch.js +0 -115
  74. package/http-client.cjs +0 -33
  75. package/http-client.js +0 -33
  76. package/index.cjs +0 -45
  77. package/index.d.cts +0 -1
  78. package/index.d.ts +0 -1
  79. package/index.js +0 -9
  80. package/io/_utils.cjs +0 -56
  81. package/io/_utils.js +0 -51
  82. package/io/buf-writer.cjs +0 -149
  83. package/io/buf-writer.js +0 -148
  84. package/io/io.cjs +0 -135
  85. package/io/io.js +0 -134
  86. package/io/readers.cjs +0 -377
  87. package/io/readers.js +0 -373
  88. package/io/writers.cjs +0 -191
  89. package/io/writers.js +0 -190
  90. package/src/_internal/consts.d.cts +0 -3
  91. package/src/_internal/consts.d.ts +0 -3
  92. package/src/_internal/error-adapters.d.cts +0 -22
  93. package/src/_internal/error-adapters.d.ts +0 -22
  94. package/src/_internal/guards.d.cts +0 -13
  95. package/src/_internal/guards.d.ts +0 -13
  96. package/src/_internal/net.d.cts +0 -12
  97. package/src/_internal/net.d.ts +0 -12
  98. package/src/_internal/promises.d.cts +0 -1
  99. package/src/_internal/promises.d.ts +0 -1
  100. package/src/_internal/streams.d.cts +0 -21
  101. package/src/_internal/streams.d.ts +0 -21
  102. package/src/agent-pool.d.cts +0 -2
  103. package/src/agent-pool.d.ts +0 -2
  104. package/src/agent.d.cts +0 -3
  105. package/src/agent.d.ts +0 -3
  106. package/src/body.d.cts +0 -23
  107. package/src/body.d.ts +0 -23
  108. package/src/dialers/index.d.cts +0 -3
  109. package/src/dialers/index.d.ts +0 -3
  110. package/src/dialers/proxy.d.cts +0 -19
  111. package/src/dialers/proxy.d.ts +0 -19
  112. package/src/dialers/tcp.d.cts +0 -36
  113. package/src/dialers/tcp.d.ts +0 -36
  114. package/src/encoding.d.cts +0 -24
  115. package/src/encoding.d.ts +0 -24
  116. package/src/errors.d.cts +0 -110
  117. package/src/errors.d.ts +0 -110
  118. package/src/fetch.d.cts +0 -36
  119. package/src/fetch.d.ts +0 -36
  120. package/src/http-client.d.cts +0 -23
  121. package/src/http-client.d.ts +0 -23
  122. package/src/index.d.cts +0 -7
  123. package/src/index.d.ts +0 -7
  124. package/src/io/_utils.d.cts +0 -10
  125. package/src/io/_utils.d.ts +0 -10
  126. package/src/io/buf-writer.d.cts +0 -13
  127. package/src/io/buf-writer.d.ts +0 -13
  128. package/src/io/io.d.cts +0 -5
  129. package/src/io/io.d.ts +0 -5
  130. package/src/io/readers.d.cts +0 -199
  131. package/src/io/readers.d.ts +0 -199
  132. package/src/io/writers.d.cts +0 -22
  133. package/src/io/writers.d.ts +0 -22
  134. package/src/types/agent.d.cts +0 -128
  135. package/src/types/agent.d.ts +0 -128
  136. package/src/types/dialer.d.ts +0 -27
  137. package/src/types/index.d.cts +0 -2
  138. package/src/types/index.d.ts +0 -2
  139. package/tests/test-utils.d.cts +0 -8
  140. package/tests/test-utils.d.ts +0 -8
@@ -0,0 +1,576 @@
1
+ import type { IClosable, IReadable } from "@fuman/io";
2
+ import { Bytes, DelimiterCodec, read as ioRead } from "@fuman/io";
3
+ import { ConnectionClosedError } from "@fuman/net";
4
+ import { CRLF_BYTES } from "../_internal/consts";
5
+ import { parseMaxBytes } from "./_utils";
6
+
7
+ type Source = IReadable & IClosable;
8
+
9
+ // FROM https://github.com/denoland/deno/blob/b34628a26ab0187a827aa4ebe256e23178e25d39/cli/js/web/headers.ts#L9
10
+ const invalidHeaderCharRegex = /[^\t\x20-\x7e\x80-\xff]/g;
11
+
12
+ export function sanitizeHeaderValue(v: string): string {
13
+ return v.replace(invalidHeaderCharRegex, (m) => encodeURI(m));
14
+ }
15
+
16
+ export namespace Readers {
17
+ export interface BufferingOptions {
18
+ bufferSize?: number;
19
+ readChunkSize?: number;
20
+ }
21
+
22
+ export interface SizeLimitOptions {
23
+ maxBodySize?: number | string;
24
+ maxDecodedBodySize?: number | string;
25
+ }
26
+
27
+ export interface DecompressionOptions {
28
+ decompress?: boolean;
29
+ }
30
+
31
+ export interface DelimiterLimitsOptions {
32
+ maxLineSize?: number;
33
+ maxBufferedBytes?: number;
34
+ }
35
+
36
+ export interface BodyOptions
37
+ extends SizeLimitOptions,
38
+ DecompressionOptions {}
39
+
40
+ export interface ChunkedOptions extends BufferingOptions {
41
+ maxLineSize?: number;
42
+ maxChunkSize?: number;
43
+ }
44
+
45
+ export type Options = LineReader.Options & BodyOptions & ChunkedOptions;
46
+ }
47
+
48
+ export class LineReader implements IReadable, IClosable {
49
+ #src: Source;
50
+ #buf: Bytes;
51
+ #codec = new DelimiterCodec(CRLF_BYTES, { strategy: "discard" });
52
+ #eof = false;
53
+
54
+ #readChunkSize: number;
55
+ #maxBufferedBytes: number;
56
+ #maxLineSize: number;
57
+ #closed = false;
58
+
59
+ close: () => Promise<void> | void;
60
+
61
+ static Options: never;
62
+
63
+ constructor(src: Source, opts: LineReader.Options = {}) {
64
+ this.#src = src;
65
+ this.#buf = Bytes.alloc(opts.bufferSize);
66
+ this.#readChunkSize = opts.readChunkSize ?? 16 * 1024;
67
+ this.#maxBufferedBytes = opts.maxBufferedBytes ?? 256 * 1024;
68
+ this.#maxLineSize = opts.maxLineSize ?? 64 * 1024;
69
+ this.close = this.#close.bind(this);
70
+ }
71
+
72
+ async read(into: Uint8Array): Promise<number> {
73
+ if (this.#closed) return 0;
74
+
75
+ if (this.#buf.available > 0) {
76
+ const n = Math.min(into.length, this.#buf.available);
77
+ into.set(this.#buf.readSync(n));
78
+ this.#buf.reclaim();
79
+ return n;
80
+ }
81
+
82
+ if (this.#eof) return 0;
83
+
84
+ try {
85
+ return await this.#src.read(into);
86
+ } catch (e) {
87
+ if (e instanceof ConnectionClosedError) {
88
+ this.#eof = true;
89
+ return 0;
90
+ }
91
+ throw e;
92
+ }
93
+ }
94
+
95
+ async readLine(): Promise<string | null> {
96
+ if (this.#closed) return null;
97
+
98
+ for (;;) {
99
+ const frame = this.#codec.decode(this.#buf, this.#eof);
100
+ if (frame !== null) {
101
+ this.#buf.reclaim();
102
+ return ioRead.rawString(frame, frame.length);
103
+ }
104
+
105
+ if (this.#eof) return null;
106
+
107
+ if (this.#buf.available > this.#maxLineSize) {
108
+ throw new Error(
109
+ `line too large (> ${this.#maxLineSize} bytes)`,
110
+ );
111
+ }
112
+
113
+ if (this.#buf.available > this.#maxBufferedBytes) {
114
+ throw new Error(
115
+ `buffer too large while searching for delimiter (> ${this.#maxBufferedBytes} bytes)`,
116
+ );
117
+ }
118
+
119
+ await this.#pull();
120
+ }
121
+ }
122
+
123
+ async readHeaders(
124
+ opts: LineReader.ReadHeadersOptions = {},
125
+ ): Promise<Headers> {
126
+ const maxHeaderSize = opts.maxHeaderSize ?? 64 * 1024;
127
+
128
+ const acc = new Map<string, string[]>();
129
+ const validator = new Headers();
130
+
131
+ let lastKey: string | null = null;
132
+ let firstLine = true;
133
+ let consumed = 0;
134
+
135
+ for (;;) {
136
+ const line = await this.readLine();
137
+ if (line === null) {
138
+ throw new Error("Unexpected EOF while reading HTTP headers");
139
+ }
140
+
141
+ consumed += line.length + 2;
142
+ if (consumed > maxHeaderSize) {
143
+ throw new Error(
144
+ `HTTP headers too large (> ${maxHeaderSize} bytes)`,
145
+ );
146
+ }
147
+
148
+ if (line === "") break;
149
+
150
+ if (firstLine && (line[0] === " " || line[0] === "\t")) {
151
+ throw new Error(`malformed HTTP header initial line: ${line}`);
152
+ }
153
+ firstLine = false;
154
+
155
+ if (line[0] === " " || line[0] === "\t") {
156
+ if (!lastKey) {
157
+ throw new Error(
158
+ `malformed HTTP header continuation line: ${line}`,
159
+ );
160
+ }
161
+ const arr = acc.get(lastKey);
162
+ if (!arr || arr.length === 0) {
163
+ throw new Error(
164
+ `malformed HTTP header continuation line: ${line}`,
165
+ );
166
+ }
167
+ const piece = sanitizeHeaderValue(line.trim());
168
+ arr[arr.length - 1] = `${arr[arr.length - 1]} ${piece}`.trim();
169
+ continue;
170
+ }
171
+
172
+ const idx = line.indexOf(":");
173
+ if (idx === -1) {
174
+ throw new Error(`malformed HTTP header line: ${line}`);
175
+ }
176
+
177
+ const rawName = line.slice(0, idx).trim();
178
+ if (rawName === "") {
179
+ lastKey = null;
180
+ continue;
181
+ }
182
+
183
+ const name = rawName.toLowerCase();
184
+ const value = sanitizeHeaderValue(line.slice(idx + 1).trim());
185
+
186
+ try {
187
+ validator.append(name, value);
188
+ } catch {
189
+ lastKey = null;
190
+ continue;
191
+ }
192
+
193
+ const arr = acc.get(name);
194
+ if (arr) arr.push(value);
195
+ else acc.set(name, [value]);
196
+
197
+ lastKey = name;
198
+ }
199
+
200
+ const headers = new Headers();
201
+ for (const [k, values] of acc) {
202
+ for (const v of values) {
203
+ try {
204
+ headers.append(k, v);
205
+ } catch {}
206
+ }
207
+ }
208
+ return headers;
209
+ }
210
+
211
+ async #pull(): Promise<void> {
212
+ const into = this.#buf.writeSync(this.#readChunkSize);
213
+ try {
214
+ const n = await this.#src.read(into);
215
+ this.#buf.disposeWriteSync(n);
216
+ if (n === 0) this.#eof = true;
217
+ } catch (e) {
218
+ this.#buf.disposeWriteSync(0);
219
+ if (e instanceof ConnectionClosedError) {
220
+ this.#eof = true;
221
+ return;
222
+ }
223
+ throw e;
224
+ } finally {
225
+ this.#buf.reclaim();
226
+ }
227
+ }
228
+
229
+ async #close(): Promise<void> {
230
+ if (this.#closed) return;
231
+ this.#closed = true;
232
+ await this.#src.close();
233
+ }
234
+ }
235
+
236
+ export namespace LineReader {
237
+ export interface Options
238
+ extends Readers.BufferingOptions,
239
+ Readers.DelimiterLimitsOptions {}
240
+
241
+ export interface ReadHeadersOptions {
242
+ maxHeaderSize?: number;
243
+ }
244
+ }
245
+
246
+ export class BodyReader implements IReadable, IClosable {
247
+ #src: Source;
248
+ #remaining: number | null;
249
+ #maxResponseSize: number | null;
250
+ #readSoFar = 0;
251
+ #closed = false;
252
+
253
+ close: () => Promise<void> | void;
254
+
255
+ static Options: never;
256
+
257
+ constructor(
258
+ src: Source,
259
+ contentLength: number | null,
260
+ opts: BodyReader.Options = {},
261
+ ) {
262
+ this.#src = src;
263
+ this.#remaining = contentLength;
264
+ this.#maxResponseSize = parseMaxBytes(opts.maxBodySize);
265
+ this.close = this.#close.bind(this);
266
+ }
267
+
268
+ async read(into: Uint8Array): Promise<number> {
269
+ if (this.#closed) return 0;
270
+
271
+ if (this.#remaining === 0) return 0;
272
+
273
+ if (this.#maxResponseSize != null) {
274
+ const remainingLimit = this.#maxResponseSize - this.#readSoFar;
275
+ if (remainingLimit <= 0) {
276
+ throw new Error(
277
+ `body too large (> ${this.#maxResponseSize} bytes)`,
278
+ );
279
+ }
280
+ if (into.length > remainingLimit) {
281
+ into = into.subarray(0, remainingLimit);
282
+ }
283
+ }
284
+
285
+ let max = into.length;
286
+ if (this.#remaining != null) max = Math.min(max, this.#remaining);
287
+ if (max === 0) return 0;
288
+
289
+ const view = max === into.length ? into : into.subarray(0, max);
290
+
291
+ let n = 0;
292
+ try {
293
+ n = await this.#src.read(view);
294
+ } catch (e) {
295
+ if (e instanceof ConnectionClosedError) n = 0;
296
+ else throw e;
297
+ }
298
+
299
+ if (n === 0) {
300
+ if (this.#remaining != null) {
301
+ throw new Error(
302
+ "Unexpected EOF while reading fixed-length body",
303
+ );
304
+ }
305
+ return 0;
306
+ }
307
+
308
+ this.#readSoFar += n;
309
+ if (this.#remaining != null) this.#remaining -= n;
310
+
311
+ return n;
312
+ }
313
+
314
+ async #close(): Promise<void> {
315
+ if (this.#closed) return;
316
+ this.#closed = true;
317
+ await this.#src.close();
318
+ }
319
+ }
320
+
321
+ export namespace BodyReader {
322
+ export interface Options {
323
+ maxBodySize?: number | string;
324
+ }
325
+ }
326
+
327
+ export class ChunkedBodyReader implements IReadable, IClosable {
328
+ #src: Source;
329
+ #buf: Bytes;
330
+ #codec = new DelimiterCodec(CRLF_BYTES, { strategy: "discard" });
331
+
332
+ #readChunkSize: number;
333
+ #maxLineSize: number;
334
+ #maxChunkSize: number;
335
+ #maxResponseSize: number | null;
336
+
337
+ #readSoFar = 0;
338
+ #eof = false;
339
+ #closed = false;
340
+
341
+ #state:
342
+ | { kind: "size" }
343
+ | { kind: "data"; remaining: number }
344
+ | { kind: "crlf" }
345
+ | { kind: "trailers" }
346
+ | { kind: "done" } = { kind: "size" };
347
+
348
+ close: () => Promise<void> | void;
349
+
350
+ static Options: never;
351
+
352
+ constructor(src: Source, opts: ChunkedBodyReader.Options = {}) {
353
+ this.#src = src;
354
+ this.#buf = Bytes.alloc(opts.bufferSize);
355
+ this.#readChunkSize = opts.readChunkSize ?? 16 * 1024;
356
+ this.#maxLineSize = opts.maxLineSize ?? 64 * 1024;
357
+ this.#maxChunkSize = opts.maxChunkSize ?? 16 * 1024 * 1024;
358
+ this.#maxResponseSize = parseMaxBytes(opts.maxBodySize);
359
+ this.close = this.#close.bind(this);
360
+ }
361
+
362
+ async read(into: Uint8Array): Promise<number> {
363
+ if (this.#closed) return 0;
364
+
365
+ for (;;) {
366
+ if (this.#state.kind === "done") return 0;
367
+
368
+ let view = into;
369
+
370
+ if (this.#maxResponseSize != null) {
371
+ const remainingLimit = this.#maxResponseSize - this.#readSoFar;
372
+ if (remainingLimit <= 0) {
373
+ throw new Error(
374
+ `body too large (> ${this.#maxResponseSize} bytes)`,
375
+ );
376
+ }
377
+ if (view.length > remainingLimit)
378
+ view = view.subarray(0, remainingLimit);
379
+ }
380
+
381
+ if (view.length === 0) return 0;
382
+
383
+ if (this.#state.kind === "data") {
384
+ if (this.#state.remaining === 0) {
385
+ this.#state = { kind: "crlf" };
386
+ continue;
387
+ }
388
+
389
+ if (this.#buf.available > 0) {
390
+ const n = Math.min(
391
+ view.length,
392
+ this.#state.remaining,
393
+ this.#buf.available,
394
+ );
395
+ view.set(this.#buf.readSync(n));
396
+ this.#buf.reclaim();
397
+
398
+ this.#readSoFar += n;
399
+ this.#state = {
400
+ kind: "data",
401
+ remaining: this.#state.remaining - n,
402
+ };
403
+ return n;
404
+ }
405
+
406
+ const max = Math.min(view.length, this.#state.remaining);
407
+ const slice =
408
+ max === view.length ? view : view.subarray(0, max);
409
+
410
+ const n = await this.#readFromSrc(slice);
411
+ if (n === 0) {
412
+ throw new Error(
413
+ "Unexpected EOF while reading chunked body",
414
+ );
415
+ }
416
+
417
+ this.#readSoFar += n;
418
+ this.#state = {
419
+ kind: "data",
420
+ remaining: this.#state.remaining - n,
421
+ };
422
+ return n;
423
+ }
424
+
425
+ if (this.#state.kind === "size") {
426
+ const line = await this.#readLine();
427
+ if (line === null) {
428
+ throw new Error("Unexpected EOF while reading chunk size");
429
+ }
430
+
431
+ const semi = line.indexOf(";");
432
+ const token = (semi === -1 ? line : line.slice(0, semi)).trim();
433
+ if (token === "") {
434
+ throw new Error(`invalid chunk size line: ${line}`);
435
+ }
436
+
437
+ const size = Number.parseInt(token, 16);
438
+ if (!Number.isFinite(size) || Number.isNaN(size) || size < 0) {
439
+ throw new Error(`invalid chunk size: ${token}`);
440
+ }
441
+
442
+ if (size > this.#maxChunkSize) {
443
+ throw new Error(
444
+ `chunk too large (> ${this.#maxChunkSize} bytes)`,
445
+ );
446
+ }
447
+
448
+ if (this.#maxResponseSize != null) {
449
+ const remainingLimit =
450
+ this.#maxResponseSize - this.#readSoFar;
451
+ if (size > remainingLimit) {
452
+ throw new Error(
453
+ `body too large (> ${this.#maxResponseSize} bytes)`,
454
+ );
455
+ }
456
+ }
457
+
458
+ this.#state =
459
+ size === 0
460
+ ? { kind: "trailers" }
461
+ : { kind: "data", remaining: size };
462
+ continue;
463
+ }
464
+
465
+ if (this.#state.kind === "crlf") {
466
+ await this.#consumeCrlf();
467
+ this.#state = { kind: "size" };
468
+ continue;
469
+ }
470
+
471
+ if (this.#state.kind === "trailers") {
472
+ for (;;) {
473
+ const line = await this.#readLine();
474
+ if (line === null) {
475
+ throw new Error(
476
+ "Unexpected EOF while reading chunked trailers",
477
+ );
478
+ }
479
+ if (line === "") {
480
+ this.#state = { kind: "done" };
481
+ return 0;
482
+ }
483
+ }
484
+ }
485
+ }
486
+ }
487
+
488
+ async #readFromSrc(into: Uint8Array): Promise<number> {
489
+ if (this.#eof) return 0;
490
+
491
+ try {
492
+ const n = await this.#src.read(into);
493
+ if (n === 0) this.#eof = true;
494
+ return n;
495
+ } catch (e) {
496
+ if (e instanceof ConnectionClosedError) {
497
+ this.#eof = true;
498
+ return 0;
499
+ }
500
+ throw e;
501
+ }
502
+ }
503
+
504
+ async #pull(): Promise<void> {
505
+ const into = this.#buf.writeSync(this.#readChunkSize);
506
+ try {
507
+ const n = await this.#readFromSrc(into);
508
+ this.#buf.disposeWriteSync(n);
509
+ } catch (e) {
510
+ this.#buf.disposeWriteSync(0);
511
+ if (e instanceof ConnectionClosedError) {
512
+ this.#eof = true;
513
+ return;
514
+ }
515
+ throw e;
516
+ } finally {
517
+ this.#buf.reclaim();
518
+ }
519
+ }
520
+
521
+ async #readLine(): Promise<string | null> {
522
+ for (;;) {
523
+ const frame = this.#codec.decode(this.#buf, this.#eof);
524
+ if (frame !== null) {
525
+ this.#buf.reclaim();
526
+ return ioRead.rawString(frame, frame.length);
527
+ }
528
+
529
+ if (this.#eof) return null;
530
+
531
+ if (this.#buf.available > this.#maxLineSize) {
532
+ throw new Error(
533
+ `chunk line too large (> ${this.#maxLineSize} bytes)`,
534
+ );
535
+ }
536
+
537
+ await this.#pull();
538
+ }
539
+ }
540
+
541
+ async #consumeCrlf(): Promise<void> {
542
+ while (this.#buf.available < 2) {
543
+ if (this.#eof) {
544
+ throw new Error(
545
+ "Unexpected EOF while reading chunk terminator",
546
+ );
547
+ }
548
+ await this.#pull();
549
+ }
550
+
551
+ const two = this.#buf.readSync(2);
552
+ this.#buf.reclaim();
553
+
554
+ if (two[0] !== CRLF_BYTES[0] || two[1] !== CRLF_BYTES[1]) {
555
+ throw new Error(
556
+ "Invalid chunked encoding: missing CRLF after chunk data",
557
+ );
558
+ }
559
+ }
560
+
561
+ async #close(): Promise<void> {
562
+ if (this.#closed) return;
563
+ this.#closed = true;
564
+ await this.#src.close();
565
+ }
566
+ }
567
+
568
+ export namespace ChunkedBodyReader {
569
+ export interface Options
570
+ extends BodyReader.Options,
571
+ Readers.BufferingOptions {
572
+ maxLineSize?: number;
573
+
574
+ maxChunkSize?: number;
575
+ }
576
+ }