@rexeus/typeweaver-server 0.10.2 → 0.10.4

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.
@@ -6,14 +6,50 @@
6
6
  */
7
7
 
8
8
  import {
9
+ badRequestDefaultError,
9
10
  createDefaultErrorBody,
10
11
  internalServerErrorDefaultError,
11
12
  payloadTooLargeDefaultError,
12
13
  } from "@rexeus/typeweaver-core";
13
- import { PayloadTooLargeError } from "./Errors.js";
14
+ import {
15
+ createNodeBodyLimitPolicy,
16
+ isBodySizeOverLimit,
17
+ markRequestBodyPrevalidated,
18
+ parseContentLength,
19
+ } from "./BodyLimitPolicy.js";
20
+ import {
21
+ PayloadTooLargeError,
22
+ RequestBodyDrainTimeoutError,
23
+ } from "./Errors.js";
24
+ import {
25
+ getTypeweaverAppErrorReporter,
26
+ getTypeweaverAppRuntimeContext,
27
+ } from "./TypeweaverInternals.js";
14
28
  import type { TypeweaverApp } from "./TypeweaverApp.js";
15
29
  import type { IncomingMessage, ServerResponse } from "node:http";
16
30
 
31
+ type DrainRequestResult = {
32
+ readonly exceededLimit: boolean;
33
+ readonly timedOut: boolean;
34
+ readonly totalBytes: number;
35
+ };
36
+
37
+ type DrainRequestOptions = {
38
+ readonly destroyOnLimitExceeded?: boolean;
39
+ readonly timeoutMs?: number;
40
+ };
41
+
42
+ const REQUEST_DRAIN_TIMEOUT_MS = 5_000;
43
+ const ORIGIN_FORM_BASE_URL_PROTOCOL = "http:";
44
+ const AUTHORITY_LIKE_REQUEST_TARGET_PREFIX = /^[\\/]{2}/;
45
+ const ASTERISK_FORM_REQUEST_TARGET = "*";
46
+
47
+ type ParsedAuthority = {
48
+ readonly host: string;
49
+ readonly hostname: string;
50
+ readonly port: string;
51
+ };
52
+
17
53
  /**
18
54
  * Adapts a `TypeweaverApp` to Node.js `http.createServer`.
19
55
  *
@@ -29,8 +65,6 @@ import type { IncomingMessage, ServerResponse } from "node:http";
29
65
  * createServer(nodeAdapter(app)).listen(3000);
30
66
  * ```
31
67
  */
32
- const DEFAULT_MAX_BODY_SIZE = 1_048_576; // 1 MB
33
-
34
68
  export type NodeAdapterOptions = {
35
69
  readonly maxBodySize?: number;
36
70
  };
@@ -39,9 +73,14 @@ export function nodeAdapter(
39
73
  app: TypeweaverApp<any>,
40
74
  options?: NodeAdapterOptions
41
75
  ): (req: IncomingMessage, res: ServerResponse) => void {
42
- const maxBodySize = options?.maxBodySize ?? DEFAULT_MAX_BODY_SIZE;
76
+ const appRuntimeContext = getTypeweaverAppRuntimeContext(app);
77
+ const maxBodySize =
78
+ options?.maxBodySize ?? appRuntimeContext?.bodyLimitPolicy.maxBodySize;
79
+ const bodyLimitPolicy = createNodeBodyLimitPolicy(maxBodySize);
80
+ const reportError = getTypeweaverAppErrorReporter(app);
81
+
43
82
  return (req, res) => {
44
- void handleRequest(app, req, res, maxBodySize);
83
+ void handleRequest(app, req, res, bodyLimitPolicy, reportError);
45
84
  };
46
85
  }
47
86
 
@@ -49,20 +88,56 @@ async function handleRequest(
49
88
  app: TypeweaverApp<any>,
50
89
  req: IncomingMessage,
51
90
  res: ServerResponse,
52
- maxBodySize: number
91
+ bodyLimitPolicy: ReturnType<typeof createNodeBodyLimitPolicy>,
92
+ reportError: (error: unknown) => void
53
93
  ): Promise<void> {
54
94
  try {
55
- const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
56
- const isBodyless = req.method === "GET" || req.method === "HEAD";
57
- const body = isBodyless ? undefined : await collectBody(req, maxBodySize);
95
+ const url = createRequestUrl(req);
96
+ if (url === undefined) {
97
+ writeBadRequestResponse(req, res, bodyLimitPolicy.maxBodySize);
98
+ return;
99
+ }
100
+ const shouldValidateBody = shouldValidateRequestBody(req.method);
101
+
102
+ enforceContentLengthLimit(req, bodyLimitPolicy.maxBodySize);
103
+
104
+ if (!shouldValidateBody && hasReadableRequestBody(req)) {
105
+ const drainResult = await drainRequest(req, bodyLimitPolicy.maxBodySize, {
106
+ destroyOnLimitExceeded: false,
107
+ });
108
+ if (drainResult.exceededLimit) {
109
+ throw new PayloadTooLargeError(
110
+ drainResult.totalBytes,
111
+ bodyLimitPolicy.maxBodySize
112
+ );
113
+ }
114
+ if (drainResult.timedOut) {
115
+ throw new RequestBodyDrainTimeoutError(
116
+ bodyLimitPolicy.maxBodySize,
117
+ REQUEST_DRAIN_TIMEOUT_MS
118
+ );
119
+ }
120
+ }
121
+
122
+ const body = !shouldValidateBody
123
+ ? undefined
124
+ : await collectBody(req, bodyLimitPolicy.maxBodySize);
58
125
 
59
126
  const request = new Request(url, {
60
127
  method: req.method,
61
- headers: req.headers as Record<string, string>,
128
+ headers: createRequestHeaders(req.headers),
62
129
  body,
63
130
  });
131
+ if (shouldValidateBody) {
132
+ markRequestBodyPrevalidated(request, bodyLimitPolicy);
133
+ }
64
134
 
65
135
  const response = await app.fetch(request);
136
+ const responseBody = await readWritableResponseBody(
137
+ req.method,
138
+ response,
139
+ reportError
140
+ );
66
141
 
67
142
  response.headers.forEach((value, key) => {
68
143
  if (key.toLowerCase() !== "set-cookie") {
@@ -74,32 +149,445 @@ async function handleRequest(
74
149
  res.setHeader("set-cookie", cookies);
75
150
  }
76
151
  res.writeHead(response.status);
77
- res.end(Buffer.from(await response.arrayBuffer()));
152
+ res.end(responseBody);
78
153
  } catch (error) {
79
- if (error instanceof PayloadTooLargeError) {
80
- if (!res.headersSent) {
81
- res.writeHead(payloadTooLargeDefaultError.statusCode, {
82
- "content-type": "application/json",
83
- });
84
- }
85
- res.end(
86
- JSON.stringify(createDefaultErrorBody(payloadTooLargeDefaultError))
87
- );
154
+ reportError(error);
155
+
156
+ if (isRequestBodyLimitError(error)) {
157
+ writeDefaultErrorResponse(res, payloadTooLargeDefaultError, {
158
+ method: req.method,
159
+ onFinished: () => {
160
+ void drainRequest(req, bodyLimitPolicy.maxBodySize, {
161
+ destroyOnLimitExceeded: true,
162
+ });
163
+ },
164
+ });
88
165
  return;
89
166
  }
90
167
 
91
- console.error(error);
92
- if (!res.headersSent) {
93
- res.writeHead(internalServerErrorDefaultError.statusCode, {
94
- "content-type": "application/json",
95
- });
168
+ writeDefaultErrorResponse(res, internalServerErrorDefaultError, {
169
+ method: req.method,
170
+ });
171
+ }
172
+ }
173
+
174
+ function createRequestUrl(req: IncomingMessage): URL | undefined {
175
+ const rawUrl = req.url ?? "/";
176
+
177
+ if (rawUrl === ASTERISK_FORM_REQUEST_TARGET) {
178
+ return createAsteriskFormRequestUrl(req);
179
+ }
180
+
181
+ if (hasAuthorityLikeRequestTargetPrefix(rawUrl)) {
182
+ return undefined;
183
+ }
184
+
185
+ try {
186
+ const url = new URL(rawUrl);
187
+ return isAbsoluteRequestHostAllowed(url, req) ? url : undefined;
188
+ } catch (error) {
189
+ if (!(error instanceof TypeError) || !rawUrl.startsWith("/")) {
190
+ throw error;
191
+ }
192
+ }
193
+
194
+ const host = parseRequestHostHeader(req, ORIGIN_FORM_BASE_URL_PROTOCOL);
195
+ if (host === undefined) {
196
+ return undefined;
197
+ }
198
+
199
+ return new URL(rawUrl, `${ORIGIN_FORM_BASE_URL_PROTOCOL}//${host.host}`);
200
+ }
201
+
202
+ function createAsteriskFormRequestUrl(req: IncomingMessage): URL | undefined {
203
+ if (req.method !== "OPTIONS") {
204
+ return undefined;
205
+ }
206
+
207
+ const host = parseRequestHostHeader(req, ORIGIN_FORM_BASE_URL_PROTOCOL);
208
+ if (host === undefined) {
209
+ return undefined;
210
+ }
211
+
212
+ return new URL(
213
+ ASTERISK_FORM_REQUEST_TARGET,
214
+ `${ORIGIN_FORM_BASE_URL_PROTOCOL}//${host.host}/`
215
+ );
216
+ }
217
+
218
+ function hasAuthorityLikeRequestTargetPrefix(rawUrl: string): boolean {
219
+ return AUTHORITY_LIKE_REQUEST_TARGET_PREFIX.test(rawUrl);
220
+ }
221
+
222
+ function isAbsoluteRequestHostAllowed(url: URL, req: IncomingMessage): boolean {
223
+ const host = parseRequestHostHeader(req, url.protocol);
224
+ if (host === undefined) {
225
+ return false;
226
+ }
227
+
228
+ const urlAuthority = getUrlAuthority(url);
229
+ return (
230
+ host.hostname.toLowerCase() === urlAuthority.hostname.toLowerCase() &&
231
+ host.port === urlAuthority.port
232
+ );
233
+ }
234
+
235
+ function parseRequestHostHeader(
236
+ req: IncomingMessage,
237
+ protocol: string
238
+ ): ParsedAuthority | undefined {
239
+ if (!hasExactlyOneHostHeaderLine(req)) {
240
+ return undefined;
241
+ }
242
+
243
+ return parseHostHeader(req.headers.host, protocol);
244
+ }
245
+
246
+ function hasExactlyOneHostHeaderLine(req: IncomingMessage): boolean {
247
+ const headersDistinctHostCount = getHeadersDistinctHostCount(req);
248
+ if (
249
+ headersDistinctHostCount !== undefined &&
250
+ headersDistinctHostCount !== 1
251
+ ) {
252
+ return false;
253
+ }
254
+
255
+ const rawHostHeaderCount = countRawHostHeaderLines(req.rawHeaders);
256
+ if (rawHostHeaderCount > 0) {
257
+ return rawHostHeaderCount === 1;
258
+ }
259
+
260
+ return headersDistinctHostCount === 1;
261
+ }
262
+
263
+ function getHeadersDistinctHostCount(req: IncomingMessage): number | undefined {
264
+ const hostHeader = req.headersDistinct?.host;
265
+ if (hostHeader === undefined) {
266
+ return undefined;
267
+ }
268
+
269
+ return Array.isArray(hostHeader) ? hostHeader.length : 1;
270
+ }
271
+
272
+ function countRawHostHeaderLines(rawHeaders: readonly string[]): number {
273
+ let count = 0;
274
+
275
+ for (let index = 0; index < rawHeaders.length; index += 2) {
276
+ if (rawHeaders[index]?.toLowerCase() === "host") {
277
+ count += 1;
278
+ }
279
+ }
280
+
281
+ return count;
282
+ }
283
+
284
+ function parseHostHeader(
285
+ hostHeader: IncomingMessage["headers"]["host"],
286
+ protocol: string
287
+ ): ParsedAuthority | undefined {
288
+ if (hostHeader === undefined || Array.isArray(hostHeader)) {
289
+ return undefined;
290
+ }
291
+
292
+ const host = hostHeader.trim();
293
+ if (host === "" || host !== hostHeader) {
294
+ return undefined;
295
+ }
296
+
297
+ try {
298
+ const parsed = new URL(`${protocol}//${host}`);
299
+ if (
300
+ parsed.username !== "" ||
301
+ parsed.password !== "" ||
302
+ parsed.pathname !== "/" ||
303
+ parsed.search !== "" ||
304
+ parsed.hash !== ""
305
+ ) {
306
+ return undefined;
96
307
  }
97
- res.end(
98
- JSON.stringify(createDefaultErrorBody(internalServerErrorDefaultError))
308
+
309
+ return getUrlAuthority(parsed);
310
+ } catch {
311
+ return undefined;
312
+ }
313
+ }
314
+
315
+ function getUrlAuthority(url: URL): ParsedAuthority {
316
+ return {
317
+ host: url.host,
318
+ hostname: url.hostname,
319
+ port: getEffectivePort(url),
320
+ };
321
+ }
322
+
323
+ function getEffectivePort(url: URL): string {
324
+ return url.port === "" ? getDefaultPort(url.protocol) : url.port;
325
+ }
326
+
327
+ function getDefaultPort(protocol: string): string {
328
+ switch (protocol) {
329
+ case "http:":
330
+ case "ws:":
331
+ return "80";
332
+ case "https:":
333
+ case "wss:":
334
+ return "443";
335
+ case "ftp:":
336
+ return "21";
337
+ default:
338
+ return "";
339
+ }
340
+ }
341
+
342
+ function shouldValidateRequestBody(method?: string): boolean {
343
+ return method !== "GET" && method !== "HEAD";
344
+ }
345
+
346
+ function shouldWriteResponseBody(
347
+ method: string | undefined,
348
+ status: number
349
+ ): boolean {
350
+ return method !== "HEAD" && status !== 204 && status !== 304;
351
+ }
352
+
353
+ async function readWritableResponseBody(
354
+ method: string | undefined,
355
+ response: Response,
356
+ reportError: (error: unknown) => void
357
+ ): Promise<Buffer | undefined> {
358
+ if (shouldWriteResponseBody(method, response.status)) {
359
+ return Buffer.from(await response.arrayBuffer());
360
+ }
361
+
362
+ cancelSuppressedResponseBody(response, reportError);
363
+ return undefined;
364
+ }
365
+
366
+ function cancelSuppressedResponseBody(
367
+ response: Response,
368
+ reportError: (error: unknown) => void
369
+ ): void {
370
+ try {
371
+ void response.body?.cancel().catch(error => {
372
+ reportSuppressedResponseBodyCancelError(error, reportError);
373
+ });
374
+ } catch (error) {
375
+ reportSuppressedResponseBodyCancelError(error, reportError);
376
+ }
377
+ }
378
+
379
+ function reportSuppressedResponseBodyCancelError(
380
+ error: unknown,
381
+ reportError: (error: unknown) => void
382
+ ): void {
383
+ try {
384
+ reportError(error);
385
+ } catch (onErrorFailure) {
386
+ console.error(
387
+ "TypeweaverApp: onError callback threw while handling error",
388
+ { onErrorFailure, originalError: error }
99
389
  );
100
390
  }
101
391
  }
102
392
 
393
+ function hasReadableRequestBody(req: IncomingMessage): boolean {
394
+ return (
395
+ req.headers["content-length"] !== undefined ||
396
+ req.headers["transfer-encoding"] !== undefined
397
+ );
398
+ }
399
+
400
+ function createRequestHeaders(headers: IncomingMessage["headers"]): Headers {
401
+ const requestHeaders = new Headers();
402
+
403
+ for (const [name, value] of Object.entries(headers)) {
404
+ if (value === undefined) {
405
+ continue;
406
+ }
407
+
408
+ if (Array.isArray(value)) {
409
+ if (name.toLowerCase() === "cookie") {
410
+ requestHeaders.set(name, value.join("; "));
411
+ continue;
412
+ }
413
+
414
+ for (const item of value) {
415
+ requestHeaders.append(name, item);
416
+ }
417
+ continue;
418
+ }
419
+
420
+ requestHeaders.set(name, value);
421
+ }
422
+
423
+ return requestHeaders;
424
+ }
425
+
426
+ function isRequestBodyLimitError(
427
+ error: unknown
428
+ ): error is PayloadTooLargeError | RequestBodyDrainTimeoutError {
429
+ return (
430
+ error instanceof PayloadTooLargeError ||
431
+ error instanceof RequestBodyDrainTimeoutError
432
+ );
433
+ }
434
+
435
+ function enforceContentLengthLimit(
436
+ req: IncomingMessage,
437
+ maxBodySize: number
438
+ ): void {
439
+ const contentLength = parseContentLength(req.headers["content-length"]);
440
+ if (contentLength === undefined) {
441
+ return;
442
+ }
443
+
444
+ if (isBodySizeOverLimit(contentLength, maxBodySize)) {
445
+ throw new PayloadTooLargeError(contentLength, maxBodySize);
446
+ }
447
+ }
448
+
449
+ function writeDefaultErrorResponse(
450
+ res: ServerResponse,
451
+ error:
452
+ | typeof badRequestDefaultError
453
+ | typeof payloadTooLargeDefaultError
454
+ | typeof internalServerErrorDefaultError,
455
+ options: {
456
+ readonly method?: string;
457
+ readonly onFinished?: () => void;
458
+ } = {}
459
+ ): void {
460
+ if (!res.headersSent) {
461
+ res.writeHead(error.statusCode, {
462
+ "content-type": "application/json",
463
+ });
464
+ }
465
+
466
+ if (options.onFinished !== undefined) {
467
+ res.once("finish", options.onFinished);
468
+ }
469
+
470
+ const body = shouldWriteResponseBody(options.method, error.statusCode)
471
+ ? JSON.stringify(createDefaultErrorBody(error))
472
+ : undefined;
473
+ res.end(body);
474
+ }
475
+
476
+ function writeBadRequestResponse(
477
+ req: IncomingMessage,
478
+ res: ServerResponse,
479
+ maxBodySize: number
480
+ ): void {
481
+ writeDefaultErrorResponse(res, badRequestDefaultError, {
482
+ method: req.method,
483
+ onFinished: createRejectedRequestBodyCleanup(req, maxBodySize),
484
+ });
485
+ }
486
+
487
+ function createRejectedRequestBodyCleanup(
488
+ req: IncomingMessage,
489
+ maxBodySize: number
490
+ ): (() => void) | undefined {
491
+ if (!hasReadableRequestBody(req)) {
492
+ return undefined;
493
+ }
494
+
495
+ return () => {
496
+ const contentLength = parseContentLength(req.headers["content-length"]);
497
+ if (
498
+ contentLength !== undefined &&
499
+ isBodySizeOverLimit(contentLength, maxBodySize)
500
+ ) {
501
+ req.destroy();
502
+ return;
503
+ }
504
+
505
+ void drainRequest(req, maxBodySize, { destroyOnLimitExceeded: true });
506
+ };
507
+ }
508
+
509
+ async function drainRequest(
510
+ req: IncomingMessage,
511
+ maxBodySize: number,
512
+ options: DrainRequestOptions = {}
513
+ ): Promise<DrainRequestResult> {
514
+ if (req.readableEnded || req.destroyed) {
515
+ return { exceededLimit: false, timedOut: false, totalBytes: 0 };
516
+ }
517
+
518
+ return await new Promise<DrainRequestResult>(resolve => {
519
+ const destroyOnLimitExceeded = options.destroyOnLimitExceeded ?? true;
520
+ const timeoutMs = options.timeoutMs ?? REQUEST_DRAIN_TIMEOUT_MS;
521
+ let drainedBytes = 0;
522
+ let isSettled = false;
523
+
524
+ const settle = (result: Omit<DrainRequestResult, "totalBytes">): void => {
525
+ if (isSettled) return;
526
+ isSettled = true;
527
+ cleanup();
528
+ resolve({ ...result, totalBytes: drainedBytes });
529
+ };
530
+
531
+ const stopReading = (
532
+ result: Omit<DrainRequestResult, "totalBytes">
533
+ ): void => {
534
+ if (isSettled) return;
535
+ isSettled = true;
536
+ cleanup();
537
+ if (destroyOnLimitExceeded) {
538
+ req.destroy();
539
+ }
540
+ resolve({ ...result, totalBytes: drainedBytes });
541
+ };
542
+
543
+ const drainTimeout = setTimeout(() => {
544
+ stopReading({ exceededLimit: false, timedOut: true });
545
+ }, timeoutMs);
546
+ drainTimeout.unref();
547
+
548
+ const handleData = (chunk: Buffer | string): void => {
549
+ drainedBytes +=
550
+ typeof chunk === "string" ? Buffer.byteLength(chunk) : chunk.byteLength;
551
+
552
+ if (isBodySizeOverLimit(drainedBytes, maxBodySize)) {
553
+ stopReading({ exceededLimit: true, timedOut: false });
554
+ }
555
+ };
556
+
557
+ const handleEnd = (): void => {
558
+ settle({ exceededLimit: false, timedOut: false });
559
+ };
560
+
561
+ const handleClose = (): void => {
562
+ settle({ exceededLimit: false, timedOut: false });
563
+ };
564
+
565
+ const handleAborted = (): void => {
566
+ settle({ exceededLimit: false, timedOut: false });
567
+ };
568
+
569
+ const handleError = (): void => {
570
+ settle({ exceededLimit: false, timedOut: false });
571
+ };
572
+
573
+ const cleanup = (): void => {
574
+ clearTimeout(drainTimeout);
575
+ req.off("data", handleData);
576
+ req.off("end", handleEnd);
577
+ req.off("error", handleError);
578
+ req.off("aborted", handleAborted);
579
+ req.off("close", handleClose);
580
+ };
581
+
582
+ req.on("data", handleData);
583
+ req.on("end", handleEnd);
584
+ req.on("error", handleError);
585
+ req.on("aborted", handleAborted);
586
+ req.on("close", handleClose);
587
+ req.resume();
588
+ });
589
+ }
590
+
103
591
  function collectBody(
104
592
  req: IncomingMessage,
105
593
  maxBodySize: number
@@ -107,26 +595,72 @@ function collectBody(
107
595
  return new Promise<ArrayBuffer>((resolve, reject) => {
108
596
  const chunks: Buffer[] = [];
109
597
  let totalBytes = 0;
598
+ let isSettled = false;
599
+
600
+ const cleanup = (): void => {
601
+ req.off("data", handleData);
602
+ req.off("end", handleEnd);
603
+ req.off("error", handleError);
604
+ req.off("aborted", handleAborted);
605
+ req.off("close", handleClose);
606
+ };
607
+
608
+ const rejectOnce = (error: unknown): void => {
609
+ if (isSettled) return;
610
+ isSettled = true;
611
+ cleanup();
612
+ reject(error);
613
+ };
614
+
615
+ const resolveOnce = (body: ArrayBuffer): void => {
616
+ if (isSettled) return;
617
+ isSettled = true;
618
+ cleanup();
619
+ resolve(body);
620
+ };
621
+
622
+ const handleData = (chunk: Buffer): void => {
623
+ if (isSettled) return;
110
624
 
111
- req.on("data", (chunk: Buffer) => {
112
625
  totalBytes += chunk.byteLength;
113
- if (totalBytes > maxBodySize) {
114
- req.destroy();
115
- reject(new PayloadTooLargeError(totalBytes, maxBodySize));
626
+ if (isBodySizeOverLimit(totalBytes, maxBodySize)) {
627
+ req.pause();
628
+ cleanup();
629
+ req.resume();
630
+ rejectOnce(new PayloadTooLargeError(totalBytes, maxBodySize));
116
631
  return;
117
632
  }
118
633
  chunks.push(chunk);
119
- });
634
+ };
120
635
 
121
- req.on("end", () => {
636
+ const handleEnd = (): void => {
122
637
  const combined = Buffer.concat(chunks, totalBytes);
123
- resolve(
638
+ resolveOnce(
124
639
  combined.buffer.slice(
125
640
  combined.byteOffset,
126
641
  combined.byteOffset + combined.byteLength
127
642
  ) as ArrayBuffer
128
643
  );
129
- });
130
- req.on("error", reject);
644
+ };
645
+
646
+ const handleError = (error: Error): void => {
647
+ rejectOnce(error);
648
+ };
649
+
650
+ const handleAborted = (): void => {
651
+ rejectOnce(new Error("Request aborted while reading body"));
652
+ };
653
+
654
+ const handleClose = (): void => {
655
+ if (!req.readableEnded) {
656
+ rejectOnce(new Error("Request closed before body was fully read"));
657
+ }
658
+ };
659
+
660
+ req.on("data", handleData);
661
+ req.on("end", handleEnd);
662
+ req.on("error", handleError);
663
+ req.on("aborted", handleAborted);
664
+ req.on("close", handleClose);
131
665
  });
132
666
  }