arthron 2.0.1 → 2.1.0

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.
package/dist/index.cjs CHANGED
@@ -183,6 +183,7 @@ async function validate(schema, body) {
183
183
  //#endregion
184
184
  //#region src/response/stream-parser.ts
185
185
  const DEFAULT_LIMIT = 8388608;
186
+ const DECODE_CHUNK_BYTES = 16384;
186
187
  /**
187
188
  * Yield the body line by line, reassembling lines split across chunks. Only
188
189
  * the newly arrived text is scanned, so a long line costs linear time; a line
@@ -199,28 +200,37 @@ async function* lines(response, maxLineLength) {
199
200
  const breaks = /\r\n|\r|\n/g;
200
201
  let pending = "";
201
202
  let afterCR = false;
203
+ const scan = function* (decoded) {
204
+ let text = decoded;
205
+ if (afterCR && text.startsWith("\n")) {
206
+ text = text.slice(1);
207
+ afterCR = false;
208
+ }
209
+ if (text) afterCR = text.endsWith("\r");
210
+ let start = 0;
211
+ breaks.lastIndex = 0;
212
+ for (let m = breaks.exec(text); m; m = breaks.exec(text)) {
213
+ yield checked(pending + text.slice(start, m.index));
214
+ pending = "";
215
+ start = breaks.lastIndex;
216
+ }
217
+ pending = checked(pending + text.slice(start));
218
+ };
202
219
  try {
203
220
  while (true) {
204
221
  const { value, done } = await reader.read();
205
- let text = decoder.decode(value, { stream: !done });
206
- if (afterCR && text.startsWith("\n")) {
207
- text = text.slice(1);
208
- afterCR = false;
222
+ if (value) for (let offset = 0; offset < value.byteLength; offset += DECODE_CHUNK_BYTES) {
223
+ const part = value.subarray(offset, offset + DECODE_CHUNK_BYTES);
224
+ yield* scan(decoder.decode(part, { stream: true }));
209
225
  }
210
- if (text) afterCR = text.endsWith("\r");
211
- let start = 0;
212
- breaks.lastIndex = 0;
213
- for (let m = breaks.exec(text); m; m = breaks.exec(text)) {
214
- yield checked(pending + text.slice(start, m.index));
215
- pending = "";
216
- start = breaks.lastIndex;
226
+ if (done) {
227
+ yield* scan(decoder.decode());
228
+ break;
217
229
  }
218
- pending = checked(pending + text.slice(start));
219
- if (done) break;
220
230
  }
221
231
  if (pending) yield pending;
222
232
  } finally {
223
- await reader.cancel().catch(() => {});
233
+ reader.cancel().catch(() => {});
224
234
  reader.releaseLock();
225
235
  }
226
236
  }
@@ -481,9 +491,9 @@ function assertSendable(url, method) {
481
491
  try {
482
492
  new Request(url, { method });
483
493
  } catch (cause) {
484
- require_create_error.redactErrorUrl(cause, url);
494
+ const safeCause = require_create_error.redactErrorUrl(cause, url);
485
495
  const reason = require_create_error.redactUrlIn(cause instanceof Error ? cause.message : String(cause), url);
486
- throw new TypeError(`arthron: cannot send ${method} ${require_create_error.redactUrl(url)} (${reason}) — use an absolute URL or a baseURL with a scheme (https://…), and a valid HTTP method`, { cause });
496
+ throw new TypeError(`arthron: cannot send ${method} ${require_create_error.redactUrl(url)} (${reason}) — use an absolute URL or a baseURL with a scheme (https://…), and a valid HTTP method`, { cause: safeCause });
487
497
  }
488
498
  }
489
499
  /**
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { C as withoutSecrets, S as resolveUrl, _ as leavesBase, a as responseError, b as redactUrl, c as isReadableStream, d as tagOf, f as ArthronError, h as httpOrigin, i as redactErrorUrl, l as runtimeStreamsRequestBody, m as fillPath, n as isCancellation, p as isArthronError, r as networkError, t as cancellationError, u as serializeBody, x as redactUrlIn, y as originSet } from "./shared.mjs";
1
+ import { C as redactUrlIn, S as redactUrl, T as withoutSecrets, _ as httpOrigin, a as redactErrorUrl, d as runtimeStreamsRequestBody, f as serializeBody, g as fillPath, h as isArthronError, i as networkError, m as ArthronError, n as isCancellation, o as responseError, p as tagOf, t as cancellationError, u as isReadableStream, w as resolveUrl, x as originSet, y as leavesBase } from "./shared.mjs";
2
2
  //#region src/request/headers.ts
3
3
  const isPlainRecord = (headers) => !Array.isArray(headers) && Object.prototype.toString.call(headers) !== "[object Headers]";
4
4
  /**
@@ -182,6 +182,7 @@ async function validate(schema, body) {
182
182
  //#endregion
183
183
  //#region src/response/stream-parser.ts
184
184
  const DEFAULT_LIMIT = 8388608;
185
+ const DECODE_CHUNK_BYTES = 16384;
185
186
  /**
186
187
  * Yield the body line by line, reassembling lines split across chunks. Only
187
188
  * the newly arrived text is scanned, so a long line costs linear time; a line
@@ -198,28 +199,37 @@ async function* lines(response, maxLineLength) {
198
199
  const breaks = /\r\n|\r|\n/g;
199
200
  let pending = "";
200
201
  let afterCR = false;
202
+ const scan = function* (decoded) {
203
+ let text = decoded;
204
+ if (afterCR && text.startsWith("\n")) {
205
+ text = text.slice(1);
206
+ afterCR = false;
207
+ }
208
+ if (text) afterCR = text.endsWith("\r");
209
+ let start = 0;
210
+ breaks.lastIndex = 0;
211
+ for (let m = breaks.exec(text); m; m = breaks.exec(text)) {
212
+ yield checked(pending + text.slice(start, m.index));
213
+ pending = "";
214
+ start = breaks.lastIndex;
215
+ }
216
+ pending = checked(pending + text.slice(start));
217
+ };
201
218
  try {
202
219
  while (true) {
203
220
  const { value, done } = await reader.read();
204
- let text = decoder.decode(value, { stream: !done });
205
- if (afterCR && text.startsWith("\n")) {
206
- text = text.slice(1);
207
- afterCR = false;
221
+ if (value) for (let offset = 0; offset < value.byteLength; offset += DECODE_CHUNK_BYTES) {
222
+ const part = value.subarray(offset, offset + DECODE_CHUNK_BYTES);
223
+ yield* scan(decoder.decode(part, { stream: true }));
208
224
  }
209
- if (text) afterCR = text.endsWith("\r");
210
- let start = 0;
211
- breaks.lastIndex = 0;
212
- for (let m = breaks.exec(text); m; m = breaks.exec(text)) {
213
- yield checked(pending + text.slice(start, m.index));
214
- pending = "";
215
- start = breaks.lastIndex;
225
+ if (done) {
226
+ yield* scan(decoder.decode());
227
+ break;
216
228
  }
217
- pending = checked(pending + text.slice(start));
218
- if (done) break;
219
229
  }
220
230
  if (pending) yield pending;
221
231
  } finally {
222
- await reader.cancel().catch(() => {});
232
+ reader.cancel().catch(() => {});
223
233
  reader.releaseLock();
224
234
  }
225
235
  }
@@ -480,9 +490,9 @@ function assertSendable(url, method) {
480
490
  try {
481
491
  new Request(url, { method });
482
492
  } catch (cause) {
483
- redactErrorUrl(cause, url);
493
+ const safeCause = redactErrorUrl(cause, url);
484
494
  const reason = redactUrlIn(cause instanceof Error ? cause.message : String(cause), url);
485
- throw new TypeError(`arthron: cannot send ${method} ${redactUrl(url)} (${reason}) — use an absolute URL or a baseURL with a scheme (https://…), and a valid HTTP method`, { cause });
495
+ throw new TypeError(`arthron: cannot send ${method} ${redactUrl(url)} (${reason}) — use an absolute URL or a baseURL with a scheme (https://…), and a valid HTTP method`, { cause: safeCause });
486
496
  }
487
497
  }
488
498
  /**
package/dist/plugins.cjs CHANGED
@@ -100,18 +100,24 @@ function bearerAuth(options) {
100
100
  }
101
101
  //#endregion
102
102
  //#region src/plugins/logging/logging.ts
103
+ /** What an error line shows without a status: the error's name (`TimeoutError`, `TypeError`), else `-`. */
104
+ function nameOf(error) {
105
+ const name = error?.name;
106
+ return typeof name === "string" && name !== "" ? name : "-";
107
+ }
103
108
  function consoleSink(logger) {
104
109
  return (e) => {
105
110
  if (e.phase === "start") logger.log(`[arthron] → ${e.method} ${e.url}`);
106
111
  else if (e.phase === "success") logger.log(`[arthron] ← ${e.method} ${e.url} ${e.status} ${e.duration}ms`);
107
- else logger.error(`[arthron] ✕ ${e.method} ${e.url} ${e.status ?? "-"} ${e.duration}ms`);
112
+ else logger.error(`[arthron] ✕ ${e.method} ${e.url} ${e.status ?? nameOf(e.error)} ${e.duration}ms`);
108
113
  };
109
114
  }
110
115
  /**
111
116
  * One event when a request starts, one when it settles, with status and
112
117
  * elapsed time. Wraps the whole call, so retries are included in the time.
113
118
  * Default output is a console line per event; pass `onEvent` to get the
114
- * structured event instead. Cancellations are not reported.
119
+ * structured event instead. Cancellations are not reported, timeouts only
120
+ * with `timeouts: true`.
115
121
  */
116
122
  function logging(options = {}) {
117
123
  if (options.now !== void 0) throw new TypeError("arthron: logging's now option left the public options in 2.0 — mock performance.now in tests (vi.spyOn(performance, \"now\"))");
@@ -119,7 +125,8 @@ function logging(options = {}) {
119
125
  }
120
126
  /** `logging()` with the clock injected — for this package's tests; `arthron/plugins` does not export it. */
121
127
  function loggingWithClock(options, now) {
122
- const { enabled = true, logger = console, onEvent } = options;
128
+ const { enabled = true, logger = console, onEvent, timeouts = false } = options;
129
+ const reported = (error, signal) => !require_create_error.isCancellation(error, signal) || timeouts && require_create_error.isTimeout(error);
123
130
  const emit = onEvent ?? consoleSink(logger);
124
131
  return {
125
132
  pluginName: "logging",
@@ -138,7 +145,7 @@ function loggingWithClock(options, now) {
138
145
  try {
139
146
  res = await next(ctx);
140
147
  } catch (error) {
141
- if (!require_create_error.isCancellation(error, ctx.init.signal)) {
148
+ if (reported(error, ctx.init.signal)) {
142
149
  const status = require_create_error.isArthronError(error) ? error.status : void 0;
143
150
  emit({
144
151
  phase: "error",
@@ -293,8 +300,10 @@ const MAX_TIMER_MS = 2 ** 31 - 1;
293
300
  /**
294
301
  * Wrap `body` so the deadline keeps running until it is fully read, and so an
295
302
  * abort while reading surfaces as a stream error instead of a silent stall.
303
+ * The idle deadline runs only while a read waits on the source: the time the
304
+ * caller takes between reads is not the server going quiet.
296
305
  */
297
- function guardBody(body, signal, finish, onChunk) {
306
+ function guardBody(body, signal, finish, idle) {
298
307
  const reader = body.getReader();
299
308
  let onAbort = () => {};
300
309
  const release = () => {
@@ -305,12 +314,15 @@ function guardBody(body, signal, finish, onChunk) {
305
314
  start(controller) {
306
315
  onAbort = () => {
307
316
  controller.error(signal.reason);
317
+ release();
308
318
  reader.cancel(signal.reason).catch(() => {});
309
319
  };
310
- signal.addEventListener("abort", onAbort, { once: true });
320
+ if (signal.aborted) onAbort();
321
+ else signal.addEventListener("abort", onAbort, { once: true });
311
322
  },
312
323
  async pull(controller) {
313
324
  let chunk;
325
+ idle.start();
314
326
  try {
315
327
  chunk = await reader.read();
316
328
  } catch (error) {
@@ -322,7 +334,7 @@ function guardBody(body, signal, finish, onChunk) {
322
334
  controller.close();
323
335
  return;
324
336
  }
325
- onChunk();
337
+ idle.stop();
326
338
  controller.enqueue(chunk.value);
327
339
  },
328
340
  cancel(reason) {
@@ -396,16 +408,19 @@ function timeout(first, removed) {
396
408
  const signal = outer ? AbortSignal.any([outer, controller.signal]) : controller.signal;
397
409
  const total = deadline === Number.POSITIVE_INFINITY ? void 0 : deadlineTimer(controller, deadline, `arthron: request timed out after ${deadline}ms`);
398
410
  let quiet;
399
- const stillAlive = () => {
400
- if (idleMs === void 0) return;
401
- clearTimeout(quiet);
402
- quiet = deadlineTimer(controller, idleMs, `arthron: no data for ${idleMs}ms`);
411
+ const idleTimer = {
412
+ start: () => {
413
+ if (idleMs === void 0) return;
414
+ clearTimeout(quiet);
415
+ quiet = deadlineTimer(controller, idleMs, `arthron: no data for ${idleMs}ms`);
416
+ },
417
+ stop: () => clearTimeout(quiet)
403
418
  };
404
419
  const finish = () => {
405
420
  clearTimeout(total);
406
- clearTimeout(quiet);
421
+ idleTimer.stop();
407
422
  };
408
- stillAlive();
423
+ idleTimer.start();
409
424
  let res;
410
425
  try {
411
426
  res = await next({
@@ -423,8 +438,8 @@ function timeout(first, removed) {
423
438
  finish();
424
439
  return res;
425
440
  }
426
- stillAlive();
427
- return require_create_error.rebuildResponse(res, guardBody(res.body, signal, finish, stillAlive));
441
+ idleTimer.stop();
442
+ return require_create_error.rebuildResponse(res, guardBody(res.body, signal, finish, idleTimer));
428
443
  }
429
444
  };
430
445
  }
@@ -449,7 +464,7 @@ function encodeFormData(form) {
449
464
  const b = boundary();
450
465
  const parts = [];
451
466
  for (const [name, value] of form) {
452
- let head = `--${b}\r\nContent-Disposition: form-data; name="${escapeName(name)}"`;
467
+ let head = `--${b}\r\nContent-Disposition: form-data; name="${escapeName(normalizeBreaks(name))}"`;
453
468
  if (typeof value === "string") {
454
469
  parts.push(encoder.encode(`${head}\r\n\r\n`));
455
470
  parts.push(encoder.encode(normalizeBreaks(value)));
@@ -711,15 +726,20 @@ function xhrFetch(onProgress) {
711
726
  xhr.onload = () => {
712
727
  if (!reported) report(0, 0);
713
728
  if (xhr.status === 0) return settle(() => reject(/* @__PURE__ */ new TypeError("Network request failed")));
714
- const headers = parseResponseHeaders(xhr.getAllResponseHeaders());
715
- const body = NULL_BODY_STATUSES.has(xhr.status) ? null : xhr.response;
716
- const res = new Response(body, {
717
- status: xhr.status,
718
- statusText: xhr.statusText,
719
- headers
720
- });
721
- const page = globalThis.location?.href;
722
- settle(() => resolve(require_create_error.withFields(res, responseFields(url, xhr.responseURL, page))));
729
+ let res;
730
+ try {
731
+ const headers = parseResponseHeaders(xhr.getAllResponseHeaders());
732
+ const body = NULL_BODY_STATUSES.has(xhr.status) ? null : xhr.response;
733
+ const page = globalThis.location?.href;
734
+ res = require_create_error.withFields(require_create_error.createResponse(body, {
735
+ status: xhr.status,
736
+ statusText: xhr.statusText,
737
+ headers
738
+ }), responseFields(url, xhr.responseURL, page));
739
+ } catch (error) {
740
+ return settle(() => reject(error));
741
+ }
742
+ settle(() => resolve(res));
723
743
  };
724
744
  xhr.onerror = xhr.ontimeout = () => settle(() => reject(/* @__PURE__ */ new TypeError("Network request failed")));
725
745
  xhr.open(init.method ?? "GET", url);
@@ -71,6 +71,14 @@ type LoggingOptions = {
71
71
  logger?: Pick<Console, "log" | "error">;
72
72
  /** Receive structured events instead of the default console lines. */
73
73
  onEvent?: (event: LogEvent) => void;
74
+ /**
75
+ * Default `false`. `true` reports a call that timed out before its response
76
+ * arrived — a `TimeoutError`, from `timeout()` or `AbortSignal.timeout()` —
77
+ * as an error event, `status` undefined. A timeout while the body is read
78
+ * rejects that read instead: the call has already logged its success.
79
+ * Other cancellations are never reported.
80
+ */
81
+ timeouts?: boolean;
74
82
  };
75
83
  //#endregion
76
84
  //#region src/plugins/logging/logging.d.ts
@@ -78,7 +86,8 @@ type LoggingOptions = {
78
86
  * One event when a request starts, one when it settles, with status and
79
87
  * elapsed time. Wraps the whole call, so retries are included in the time.
80
88
  * Default output is a console line per event; pass `onEvent` to get the
81
- * structured event instead. Cancellations are not reported.
89
+ * structured event instead. Cancellations are not reported, timeouts only
90
+ * with `timeouts: true`.
82
91
  */
83
92
  export declare function logging(options?: LoggingOptions): Plugin;
84
93
  //#endregion
@@ -71,6 +71,14 @@ type LoggingOptions = {
71
71
  logger?: Pick<Console, "log" | "error">;
72
72
  /** Receive structured events instead of the default console lines. */
73
73
  onEvent?: (event: LogEvent) => void;
74
+ /**
75
+ * Default `false`. `true` reports a call that timed out before its response
76
+ * arrived — a `TimeoutError`, from `timeout()` or `AbortSignal.timeout()` —
77
+ * as an error event, `status` undefined. A timeout while the body is read
78
+ * rejects that read instead: the call has already logged its success.
79
+ * Other cancellations are never reported.
80
+ */
81
+ timeouts?: boolean;
74
82
  };
75
83
  //#endregion
76
84
  //#region src/plugins/logging/logging.d.ts
@@ -78,7 +86,8 @@ type LoggingOptions = {
78
86
  * One event when a request starts, one when it settles, with status and
79
87
  * elapsed time. Wraps the whole call, so retries are included in the time.
80
88
  * Default output is a console line per event; pass `onEvent` to get the
81
- * structured event instead. Cancellations are not reported.
89
+ * structured event instead. Cancellations are not reported, timeouts only
90
+ * with `timeouts: true`.
82
91
  */
83
92
  export declare function logging(options?: LoggingOptions): Plugin;
84
93
  //#endregion
package/dist/plugins.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { b as redactUrl, c as isReadableStream, d as tagOf, g as httpOrigins, n as isCancellation, o as rebuildResponse, p as isArthronError, s as withFields, v as originOf } from "./shared.mjs";
1
+ import { S as redactUrl, b as originOf, c as rebuildResponse, h as isArthronError, l as withFields, n as isCancellation, p as tagOf, r as isTimeout, s as createResponse, u as isReadableStream, v as httpOrigins } from "./shared.mjs";
2
2
  //#region src/plugins/bearer-auth/bearer-auth.ts
3
3
  /** 1.x option, its replacement, and how to pass it. */
4
4
  const REPLACED$1 = [
@@ -99,18 +99,24 @@ function bearerAuth(options) {
99
99
  }
100
100
  //#endregion
101
101
  //#region src/plugins/logging/logging.ts
102
+ /** What an error line shows without a status: the error's name (`TimeoutError`, `TypeError`), else `-`. */
103
+ function nameOf(error) {
104
+ const name = error?.name;
105
+ return typeof name === "string" && name !== "" ? name : "-";
106
+ }
102
107
  function consoleSink(logger) {
103
108
  return (e) => {
104
109
  if (e.phase === "start") logger.log(`[arthron] → ${e.method} ${e.url}`);
105
110
  else if (e.phase === "success") logger.log(`[arthron] ← ${e.method} ${e.url} ${e.status} ${e.duration}ms`);
106
- else logger.error(`[arthron] ✕ ${e.method} ${e.url} ${e.status ?? "-"} ${e.duration}ms`);
111
+ else logger.error(`[arthron] ✕ ${e.method} ${e.url} ${e.status ?? nameOf(e.error)} ${e.duration}ms`);
107
112
  };
108
113
  }
109
114
  /**
110
115
  * One event when a request starts, one when it settles, with status and
111
116
  * elapsed time. Wraps the whole call, so retries are included in the time.
112
117
  * Default output is a console line per event; pass `onEvent` to get the
113
- * structured event instead. Cancellations are not reported.
118
+ * structured event instead. Cancellations are not reported, timeouts only
119
+ * with `timeouts: true`.
114
120
  */
115
121
  function logging(options = {}) {
116
122
  if (options.now !== void 0) throw new TypeError("arthron: logging's now option left the public options in 2.0 — mock performance.now in tests (vi.spyOn(performance, \"now\"))");
@@ -118,7 +124,8 @@ function logging(options = {}) {
118
124
  }
119
125
  /** `logging()` with the clock injected — for this package's tests; `arthron/plugins` does not export it. */
120
126
  function loggingWithClock(options, now) {
121
- const { enabled = true, logger = console, onEvent } = options;
127
+ const { enabled = true, logger = console, onEvent, timeouts = false } = options;
128
+ const reported = (error, signal) => !isCancellation(error, signal) || timeouts && isTimeout(error);
122
129
  const emit = onEvent ?? consoleSink(logger);
123
130
  return {
124
131
  pluginName: "logging",
@@ -137,7 +144,7 @@ function loggingWithClock(options, now) {
137
144
  try {
138
145
  res = await next(ctx);
139
146
  } catch (error) {
140
- if (!isCancellation(error, ctx.init.signal)) {
147
+ if (reported(error, ctx.init.signal)) {
141
148
  const status = isArthronError(error) ? error.status : void 0;
142
149
  emit({
143
150
  phase: "error",
@@ -292,8 +299,10 @@ const MAX_TIMER_MS = 2 ** 31 - 1;
292
299
  /**
293
300
  * Wrap `body` so the deadline keeps running until it is fully read, and so an
294
301
  * abort while reading surfaces as a stream error instead of a silent stall.
302
+ * The idle deadline runs only while a read waits on the source: the time the
303
+ * caller takes between reads is not the server going quiet.
295
304
  */
296
- function guardBody(body, signal, finish, onChunk) {
305
+ function guardBody(body, signal, finish, idle) {
297
306
  const reader = body.getReader();
298
307
  let onAbort = () => {};
299
308
  const release = () => {
@@ -304,12 +313,15 @@ function guardBody(body, signal, finish, onChunk) {
304
313
  start(controller) {
305
314
  onAbort = () => {
306
315
  controller.error(signal.reason);
316
+ release();
307
317
  reader.cancel(signal.reason).catch(() => {});
308
318
  };
309
- signal.addEventListener("abort", onAbort, { once: true });
319
+ if (signal.aborted) onAbort();
320
+ else signal.addEventListener("abort", onAbort, { once: true });
310
321
  },
311
322
  async pull(controller) {
312
323
  let chunk;
324
+ idle.start();
313
325
  try {
314
326
  chunk = await reader.read();
315
327
  } catch (error) {
@@ -321,7 +333,7 @@ function guardBody(body, signal, finish, onChunk) {
321
333
  controller.close();
322
334
  return;
323
335
  }
324
- onChunk();
336
+ idle.stop();
325
337
  controller.enqueue(chunk.value);
326
338
  },
327
339
  cancel(reason) {
@@ -395,16 +407,19 @@ function timeout(first, removed) {
395
407
  const signal = outer ? AbortSignal.any([outer, controller.signal]) : controller.signal;
396
408
  const total = deadline === Number.POSITIVE_INFINITY ? void 0 : deadlineTimer(controller, deadline, `arthron: request timed out after ${deadline}ms`);
397
409
  let quiet;
398
- const stillAlive = () => {
399
- if (idleMs === void 0) return;
400
- clearTimeout(quiet);
401
- quiet = deadlineTimer(controller, idleMs, `arthron: no data for ${idleMs}ms`);
410
+ const idleTimer = {
411
+ start: () => {
412
+ if (idleMs === void 0) return;
413
+ clearTimeout(quiet);
414
+ quiet = deadlineTimer(controller, idleMs, `arthron: no data for ${idleMs}ms`);
415
+ },
416
+ stop: () => clearTimeout(quiet)
402
417
  };
403
418
  const finish = () => {
404
419
  clearTimeout(total);
405
- clearTimeout(quiet);
420
+ idleTimer.stop();
406
421
  };
407
- stillAlive();
422
+ idleTimer.start();
408
423
  let res;
409
424
  try {
410
425
  res = await next({
@@ -422,8 +437,8 @@ function timeout(first, removed) {
422
437
  finish();
423
438
  return res;
424
439
  }
425
- stillAlive();
426
- return rebuildResponse(res, guardBody(res.body, signal, finish, stillAlive));
440
+ idleTimer.stop();
441
+ return rebuildResponse(res, guardBody(res.body, signal, finish, idleTimer));
427
442
  }
428
443
  };
429
444
  }
@@ -448,7 +463,7 @@ function encodeFormData(form) {
448
463
  const b = boundary();
449
464
  const parts = [];
450
465
  for (const [name, value] of form) {
451
- let head = `--${b}\r\nContent-Disposition: form-data; name="${escapeName(name)}"`;
466
+ let head = `--${b}\r\nContent-Disposition: form-data; name="${escapeName(normalizeBreaks(name))}"`;
452
467
  if (typeof value === "string") {
453
468
  parts.push(encoder.encode(`${head}\r\n\r\n`));
454
469
  parts.push(encoder.encode(normalizeBreaks(value)));
@@ -710,15 +725,20 @@ function xhrFetch(onProgress) {
710
725
  xhr.onload = () => {
711
726
  if (!reported) report(0, 0);
712
727
  if (xhr.status === 0) return settle(() => reject(/* @__PURE__ */ new TypeError("Network request failed")));
713
- const headers = parseResponseHeaders(xhr.getAllResponseHeaders());
714
- const body = NULL_BODY_STATUSES.has(xhr.status) ? null : xhr.response;
715
- const res = new Response(body, {
716
- status: xhr.status,
717
- statusText: xhr.statusText,
718
- headers
719
- });
720
- const page = globalThis.location?.href;
721
- settle(() => resolve(withFields(res, responseFields(url, xhr.responseURL, page))));
728
+ let res;
729
+ try {
730
+ const headers = parseResponseHeaders(xhr.getAllResponseHeaders());
731
+ const body = NULL_BODY_STATUSES.has(xhr.status) ? null : xhr.response;
732
+ const page = globalThis.location?.href;
733
+ res = withFields(createResponse(body, {
734
+ status: xhr.status,
735
+ statusText: xhr.statusText,
736
+ headers
737
+ }), responseFields(url, xhr.responseURL, page));
738
+ } catch (error) {
739
+ return settle(() => reject(error));
740
+ }
741
+ settle(() => resolve(res));
722
742
  };
723
743
  xhr.onerror = xhr.ontimeout = () => settle(() => reject(/* @__PURE__ */ new TypeError("Network request failed")));
724
744
  xhr.open(init.method ?? "GET", url);