copillm 0.1.4 → 0.2.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.
@@ -10,6 +10,7 @@ import { translateOpenAIStreamToAnthropic, writeAnthropicPrelude } from "../tran
10
10
  import { buildCodexCatalog } from "./codexSchema.js";
11
11
  import { getGithubUserSummary, GithubUserFetchError } from "./debugInfo.js";
12
12
  import { buildAnthropicModelsResponse } from "./anthropicModelsResponse.js";
13
+ import { attachRequestLifecycle, isBenignSocketError, safeEnd, safeSendJson, safeWrite } from "./requestLifecycle.js";
13
14
  const COPILOT_HEADERS = {
14
15
  "Content-Type": "application/json",
15
16
  "Copilot-Integration-Id": "vscode-chat",
@@ -49,6 +50,7 @@ export async function startProxyServer(input) {
49
50
  const requestId = randomUUID();
50
51
  const startedAt = Date.now();
51
52
  const pathname = safePathname(req.url);
53
+ const lifecycle = attachRequestLifecycle(req, res, input.logger, requestId);
52
54
  res.on("finish", () => {
53
55
  input.logger.info({
54
56
  event: "http_request",
@@ -148,6 +150,7 @@ export async function startProxyServer(input) {
148
150
  }
149
151
  await handleDebug(res, {
150
152
  config: input.config,
153
+ logger: input.logger,
151
154
  tokenManager: input.tokenManager,
152
155
  githubToken: input.githubToken,
153
156
  port: input.port
@@ -192,6 +195,17 @@ export async function startProxyServer(input) {
192
195
  beginAnthropicSseResponse(res, req);
193
196
  prelude = writeAnthropicPrelude(res, requestedModel ?? "");
194
197
  }
198
+ input.logger.debug({
199
+ event: "request_prepared",
200
+ request_id: requestId,
201
+ route: route.kind,
202
+ anthro_shape: route.anthroShape,
203
+ requested_model: requestedModel,
204
+ upstream_model: readRequestedModel(upstreamBody),
205
+ model_resolution_rule: resolvedModel?.rule ?? null,
206
+ upstream_path: upstreamPath,
207
+ ...summarizeUpstreamPayload(upstreamBody)
208
+ }, "prepared upstream request");
195
209
  try {
196
210
  const upstream = await postToCopilot({
197
211
  tokenManager: input.tokenManager,
@@ -199,11 +213,22 @@ export async function startProxyServer(input) {
199
213
  body: upstreamBody,
200
214
  requestId,
201
215
  logger: input.logger,
202
- upstreamPath
216
+ upstreamPath,
217
+ signal: lifecycle.signal
218
+ });
219
+ await forwardResponse(upstream, route.anthroShape, res, {
220
+ requestedModel: requestedModel ?? undefined,
221
+ prelude,
222
+ logger: input.logger,
223
+ requestId
203
224
  });
204
- await forwardResponse(upstream, route.anthroShape, res, requestedModel ?? undefined, prelude);
205
225
  }
206
226
  catch (error) {
227
+ if (isBenignSocketError(error)) {
228
+ input.logger.debug({ event: "upstream_aborted", request_id: requestId, err: error }, "upstream request aborted (client disconnected)");
229
+ safeEnd(res);
230
+ return;
231
+ }
207
232
  if (error instanceof CopilotTokenManagerError) {
208
233
  if (prelude) {
209
234
  writeAnthropicSseError(res, prelude, "token_refresh_failed");
@@ -232,8 +257,31 @@ export async function startProxyServer(input) {
232
257
  sendJson(res, 400, { error: error.code, detail: error.message });
233
258
  return;
234
259
  }
235
- input.logger.error({ err: error }, "request failed");
260
+ if (isBenignSocketError(error)) {
261
+ input.logger.debug({ event: "request_client_gone", request_id: requestId, err: error }, "request handler aborted because client disconnected");
262
+ safeEnd(res);
263
+ return;
264
+ }
265
+ input.logger.error({ err: error, request_id: requestId }, "request failed");
236
266
  sendJson(res, 500, { error: "internal_error" });
267
+ safeEnd(res);
268
+ }
269
+ });
270
+ server.on("clientError", (err, socket) => {
271
+ input.logger.debug({ event: "client_error", code: err?.code }, "malformed HTTP from client");
272
+ if (socket.writable) {
273
+ try {
274
+ socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
275
+ }
276
+ catch {
277
+ // Socket likely already gone — nothing to do.
278
+ }
279
+ }
280
+ try {
281
+ socket.destroy();
282
+ }
283
+ catch {
284
+ // best effort
237
285
  }
238
286
  });
239
287
  await new Promise((resolve, reject) => {
@@ -256,8 +304,29 @@ async function postToCopilot(input) {
256
304
  let forceRefresh = false;
257
305
  let authRefreshRetried = false;
258
306
  for (let attempt = 1; attempt <= MAX_UPSTREAM_ATTEMPTS; attempt += 1) {
307
+ if (input.signal?.aborted) {
308
+ throw abortErrorFromSignal(input.signal);
309
+ }
259
310
  try {
260
- const response = await postWithCurrentBearer(input.tokenManager, input.accountType, input.body, forceRefresh, input.requestId, input.upstreamPath);
311
+ const attemptStartedAt = Date.now();
312
+ input.logger.debug({
313
+ event: "upstream_request",
314
+ request_id: input.requestId,
315
+ attempt,
316
+ upstream_path: input.upstreamPath,
317
+ force_refresh: forceRefresh
318
+ }, "posting upstream request");
319
+ const response = await postWithCurrentBearer(input.tokenManager, input.accountType, input.body, forceRefresh, input.requestId, input.upstreamPath, input.signal);
320
+ input.logger.debug({
321
+ event: "upstream_response",
322
+ request_id: input.requestId,
323
+ attempt,
324
+ upstream_path: input.upstreamPath,
325
+ status_code: response.status,
326
+ duration_ms: Date.now() - attemptStartedAt,
327
+ content_type: response.headers.get("content-type"),
328
+ retry_after: response.headers.get("retry-after")
329
+ }, "received upstream response");
261
330
  forceRefresh = false;
262
331
  if (response.status === 401 && !authRefreshRetried && attempt < MAX_UPSTREAM_ATTEMPTS) {
263
332
  authRefreshRetried = true;
@@ -275,6 +344,10 @@ async function postToCopilot(input) {
275
344
  return response;
276
345
  }
277
346
  catch (error) {
347
+ if (isBenignSocketError(error)) {
348
+ // Client disconnected — propagate so the request handler can clean up.
349
+ throw error;
350
+ }
278
351
  if (!isRetryableTransportError(error) || attempt >= MAX_UPSTREAM_ATTEMPTS) {
279
352
  throw error;
280
353
  }
@@ -284,7 +357,7 @@ async function postToCopilot(input) {
284
357
  }
285
358
  throw new Error("Upstream retry budget exhausted unexpectedly.");
286
359
  }
287
- async function postWithCurrentBearer(tokenManager, accountType, body, forceRefresh, requestId, upstreamPath) {
360
+ async function postWithCurrentBearer(tokenManager, accountType, body, forceRefresh, requestId, upstreamPath, signal) {
288
361
  const bearer = await tokenManager.ensureToken({ forceRefresh });
289
362
  return fetch(`${accountBaseUrl(accountType)}${upstreamPath}`, {
290
363
  method: "POST",
@@ -293,46 +366,70 @@ async function postWithCurrentBearer(tokenManager, accountType, body, forceRefre
293
366
  Authorization: `Bearer ${bearer}`,
294
367
  "X-Request-Id": requestId
295
368
  },
296
- body: JSON.stringify(body)
369
+ body: JSON.stringify(body),
370
+ signal
297
371
  });
298
372
  }
299
- async function forwardResponse(upstream, anthroShape, res, requestedModel, prelude) {
373
+ function abortErrorFromSignal(signal) {
374
+ const reason = signal.reason;
375
+ if (reason instanceof Error) {
376
+ return reason;
377
+ }
378
+ const err = new Error("Request aborted by client");
379
+ err.name = "AbortError";
380
+ return err;
381
+ }
382
+ async function forwardResponse(upstream, anthroShape, res, diagnostics) {
300
383
  if (!upstream.ok) {
301
- await discardUpstreamBody(upstream);
384
+ const upstreamError = await readUpstreamError(upstream);
385
+ const category = upstreamStatusCategory(upstream.status);
386
+ diagnostics.logger.warn({
387
+ event: "upstream_non_ok",
388
+ request_id: diagnostics.requestId,
389
+ status_code: upstream.status,
390
+ error: category,
391
+ upstream_content_type: upstreamError.contentType,
392
+ upstream_error_code: upstreamError.code,
393
+ upstream_error_type: upstreamError.type,
394
+ upstream_error_message: upstreamError.message,
395
+ upstream_response_bytes: upstreamError.responseBytes
396
+ }, "upstream request failed");
397
+ const message = formatUpstreamErrorMessage(category, upstreamError);
398
+ const prelude = diagnostics.prelude ?? null;
302
399
  if (prelude) {
303
- writeAnthropicSseError(res, prelude, upstreamStatusCategory(upstream.status));
400
+ writeAnthropicSseError(res, prelude, message);
304
401
  return;
305
402
  }
306
- sendJson(res, upstream.status, { error: upstreamStatusCategory(upstream.status) });
403
+ sendJson(res, upstream.status, buildUpstreamErrorPayload(category, upstream.status, diagnostics.requestId, upstreamError, anthroShape));
307
404
  return;
308
405
  }
309
406
  if (isEventStream(upstream)) {
310
407
  if (anthroShape) {
311
408
  if (!upstream.body) {
312
- if (prelude) {
313
- writeAnthropicSseError(res, prelude, "invalid_upstream_response");
409
+ if (diagnostics.prelude) {
410
+ writeAnthropicSseError(res, diagnostics.prelude, "invalid_upstream_response");
314
411
  return;
315
412
  }
316
413
  sendJson(res, 502, { error: "invalid_upstream_response", detail: "Upstream stream body is missing." });
317
414
  return;
318
415
  }
319
- if (!prelude) {
416
+ if (!diagnostics.prelude) {
320
417
  beginAnthropicSseResponse(res);
321
418
  }
322
419
  const upstreamReadable = Readable.fromWeb(upstream.body);
323
420
  await translateOpenAIStreamToAnthropic({
324
421
  upstream: upstreamReadable,
325
422
  downstream: res,
326
- fallbackModel: requestedModel,
327
- preEmittedMessageId: prelude?.messageId
423
+ fallbackModel: diagnostics.requestedModel,
424
+ preEmittedMessageId: diagnostics.prelude?.messageId
328
425
  });
329
426
  return;
330
427
  }
331
428
  await pipeEventStream(upstream, res);
332
429
  return;
333
430
  }
334
- if (prelude) {
335
- writeAnthropicSseError(res, prelude, "invalid_upstream_response");
431
+ if (diagnostics.prelude) {
432
+ writeAnthropicSseError(res, diagnostics.prelude, "invalid_upstream_response");
336
433
  return;
337
434
  }
338
435
  let json;
@@ -386,7 +483,17 @@ async function pipeEventStream(upstream, res) {
386
483
  if (res.socket && typeof res.socket.setNoDelay === "function") {
387
484
  res.socket.setNoDelay(true);
388
485
  }
389
- await pipeline(Readable.fromWeb(upstream.body), res);
486
+ try {
487
+ await pipeline(Readable.fromWeb(upstream.body), res);
488
+ }
489
+ catch (error) {
490
+ if (isBenignSocketError(error)) {
491
+ // Client went away mid-stream — normal for SSE consumers (Codex,
492
+ // Claude Code, pi) that cancel pending responses on user input.
493
+ return;
494
+ }
495
+ throw error;
496
+ }
390
497
  }
391
498
  function isEventStream(upstream) {
392
499
  const contentType = upstream.headers.get("content-type");
@@ -415,16 +522,16 @@ function beginAnthropicSseResponse(res, req) {
415
522
  export function writeAnthropicSseError(res, prelude, code) {
416
523
  void prelude;
417
524
  try {
418
- res.write(`event: message_delta\ndata: ${JSON.stringify({
525
+ safeWrite(res, `event: message_delta\ndata: ${JSON.stringify({
419
526
  type: "message_delta",
420
527
  delta: { stop_reason: "end_turn", stop_sequence: null },
421
528
  usage: { input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0 }
422
529
  })}\n\n`);
423
- res.write(`event: error\ndata: ${JSON.stringify({ type: "error", error: { type: "api_error", message: code } })}\n\n`);
424
- res.write(`event: message_stop\ndata: ${JSON.stringify({ type: "message_stop" })}\n\n`);
530
+ safeWrite(res, `event: error\ndata: ${JSON.stringify({ type: "error", error: { type: "api_error", message: code } })}\n\n`);
531
+ safeWrite(res, `event: message_stop\ndata: ${JSON.stringify({ type: "message_stop" })}\n\n`);
425
532
  }
426
533
  finally {
427
- res.end();
534
+ safeEnd(res);
428
535
  }
429
536
  }
430
537
  function isLocalRequest(req) {
@@ -455,9 +562,7 @@ async function readJson(req) {
455
562
  }
456
563
  }
457
564
  function sendJson(res, status, payload) {
458
- res.statusCode = status;
459
- res.setHeader("Content-Type", "application/json");
460
- res.end(JSON.stringify(payload));
565
+ safeSendJson(res, status, payload);
461
566
  }
462
567
  function readRequestedModel(payload) {
463
568
  if (!payload || typeof payload !== "object") {
@@ -548,7 +653,9 @@ async function handleDebug(res, input) {
548
653
  uptime_seconds: uptimeSeconds,
549
654
  account_type: input.config.accountType,
550
655
  selected_models: input.config.selectedModels,
551
- require_caller_secret: input.config.requireCallerSecret
656
+ require_caller_secret: input.config.requireCallerSecret,
657
+ log_level: input.logger.level,
658
+ log_file: process.env.COPILLM_LOG_FILE ?? null
552
659
  },
553
660
  auth: {
554
661
  bearer_ttl_seconds: bearerTtlSeconds,
@@ -656,6 +763,165 @@ function upstreamStatusCategory(status) {
656
763
  }
657
764
  return "upstream_error";
658
765
  }
766
+ async function readUpstreamError(response) {
767
+ const contentType = response.headers.get("content-type");
768
+ let text;
769
+ try {
770
+ text = await response.text();
771
+ }
772
+ catch {
773
+ return { contentType, code: null, type: null, message: null, responseBytes: null };
774
+ }
775
+ const trimmed = text.trim();
776
+ const responseBytes = Buffer.byteLength(text, "utf8");
777
+ if (trimmed.length === 0) {
778
+ return { contentType, code: null, type: null, message: null, responseBytes };
779
+ }
780
+ try {
781
+ const parsed = JSON.parse(trimmed);
782
+ const extracted = extractErrorFields(parsed);
783
+ if (extracted.message || extracted.code || extracted.type) {
784
+ return { contentType, responseBytes, ...extracted };
785
+ }
786
+ }
787
+ catch {
788
+ // Fall through to a plain-text snippet below.
789
+ }
790
+ return {
791
+ contentType,
792
+ code: null,
793
+ type: null,
794
+ message: truncateForDiagnostics(trimmed),
795
+ responseBytes
796
+ };
797
+ }
798
+ function extractErrorFields(payload) {
799
+ if (!payload || typeof payload !== "object") {
800
+ return { code: null, type: null, message: typeof payload === "string" ? truncateForDiagnostics(payload) : null };
801
+ }
802
+ const record = payload;
803
+ const nested = record.error;
804
+ if (typeof nested === "string") {
805
+ return {
806
+ code: readStringField(record, "code"),
807
+ type: readStringField(record, "type"),
808
+ message: truncateForDiagnostics(nested)
809
+ };
810
+ }
811
+ if (nested && typeof nested === "object") {
812
+ const errorRecord = nested;
813
+ return {
814
+ code: readStringField(errorRecord, "code") ?? readStringField(record, "code"),
815
+ type: readStringField(errorRecord, "type") ?? readStringField(record, "type"),
816
+ message: readTruncatedStringField(errorRecord, "message") ??
817
+ readTruncatedStringField(errorRecord, "detail") ??
818
+ readTruncatedStringField(record, "message") ??
819
+ readTruncatedStringField(record, "detail")
820
+ };
821
+ }
822
+ return {
823
+ code: readStringField(record, "code"),
824
+ type: readStringField(record, "type"),
825
+ message: readTruncatedStringField(record, "message") ?? readTruncatedStringField(record, "detail")
826
+ };
827
+ }
828
+ function buildUpstreamErrorPayload(category, statusCode, requestId, upstreamError, anthroShape) {
829
+ const code = upstreamError.code ?? category;
830
+ const type = upstreamError.type ?? category;
831
+ const message = formatUserFacingUpstreamErrorMessage(category, upstreamError);
832
+ if (anthroShape) {
833
+ return {
834
+ type: "error",
835
+ error: {
836
+ type,
837
+ message,
838
+ code,
839
+ upstream_status_code: statusCode,
840
+ request_id: requestId
841
+ }
842
+ };
843
+ }
844
+ return {
845
+ error: {
846
+ type,
847
+ code,
848
+ message,
849
+ upstream_status_code: statusCode,
850
+ request_id: requestId
851
+ }
852
+ };
853
+ }
854
+ function formatUpstreamErrorMessage(category, upstreamError) {
855
+ const parts = [upstreamError.code, upstreamError.type, upstreamError.message].filter((part) => typeof part === "string" && part.length > 0);
856
+ return parts.length > 0 ? `${category}: ${parts.join(": ")}` : category;
857
+ }
858
+ function formatUserFacingUpstreamErrorMessage(category, upstreamError) {
859
+ if (upstreamError.code && upstreamError.message) {
860
+ return `${upstreamError.code}: ${upstreamError.message}`;
861
+ }
862
+ if (upstreamError.message) {
863
+ return upstreamError.message;
864
+ }
865
+ return upstreamError.code ?? upstreamError.type ?? category;
866
+ }
867
+ function readStringField(record, key) {
868
+ const value = record[key];
869
+ return typeof value === "string" && value.length > 0 ? value : null;
870
+ }
871
+ function readTruncatedStringField(record, key) {
872
+ const value = readStringField(record, key);
873
+ return value ? truncateForDiagnostics(value) : null;
874
+ }
875
+ function truncateForDiagnostics(value) {
876
+ const maxChars = 500;
877
+ return value.length > maxChars ? `${value.slice(0, maxChars)}...` : value;
878
+ }
879
+ function summarizeUpstreamPayload(payload) {
880
+ let requestBytes = null;
881
+ try {
882
+ requestBytes = Buffer.byteLength(JSON.stringify(payload), "utf8");
883
+ }
884
+ catch {
885
+ requestBytes = null;
886
+ }
887
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
888
+ return { upstream_request_bytes: requestBytes };
889
+ }
890
+ const record = payload;
891
+ const messages = Array.isArray(record.messages) ? record.messages : null;
892
+ const input = Array.isArray(record.input) ? record.input : null;
893
+ return {
894
+ upstream_request_bytes: requestBytes,
895
+ stream: record.stream === true,
896
+ max_tokens: typeof record.max_tokens === "number" ? record.max_tokens : null,
897
+ message_count: messages?.length ?? null,
898
+ input_item_count: input?.length ?? null,
899
+ tool_count: Array.isArray(record.tools) ? record.tools.length : 0,
900
+ text_characters: sumTextCharacters(payload)
901
+ };
902
+ }
903
+ function sumTextCharacters(value) {
904
+ if (typeof value === "string") {
905
+ return value.length;
906
+ }
907
+ if (!value || typeof value !== "object") {
908
+ return 0;
909
+ }
910
+ if (Array.isArray(value)) {
911
+ return value.reduce((total, item) => total + sumTextCharacters(item), 0);
912
+ }
913
+ let total = 0;
914
+ const record = value;
915
+ for (const [key, nested] of Object.entries(record)) {
916
+ if (key === "text" || key === "content" || key === "arguments" || key === "input") {
917
+ total += sumTextCharacters(nested);
918
+ }
919
+ else if (nested && typeof nested === "object" && key !== "data" && key !== "image_url" && key !== "source") {
920
+ total += sumTextCharacters(nested);
921
+ }
922
+ }
923
+ return total;
924
+ }
659
925
  function healthFailure(error) {
660
926
  if (error instanceof CopilotTokenExchangeError) {
661
927
  if (error.statusCode === 401 || error.statusCode === 403) {
@@ -0,0 +1,115 @@
1
+ // Node error codes we treat as "client went away, this is normal" — they
2
+ // must never crash the daemon and must not be reported as errors. SSE clients
3
+ // (Codex, Claude Code, pi) abort streams constantly: user types another
4
+ // prompt, hits Esc, switches turns, etc.
5
+ const BENIGN_SOCKET_ERROR_CODES = new Set([
6
+ "ECONNRESET",
7
+ "EPIPE",
8
+ "ERR_STREAM_PREMATURE_CLOSE",
9
+ "ERR_STREAM_DESTROYED",
10
+ "ERR_STREAM_WRITE_AFTER_END",
11
+ "ERR_HTTP_HEADERS_SENT"
12
+ ]);
13
+ export function isBenignSocketError(error) {
14
+ if (!error || typeof error !== "object") {
15
+ return false;
16
+ }
17
+ const err = error;
18
+ if (typeof err.code === "string" && BENIGN_SOCKET_ERROR_CODES.has(err.code)) {
19
+ return true;
20
+ }
21
+ if (err.name === "AbortError") {
22
+ return true;
23
+ }
24
+ if (err.cause && isBenignSocketError(err.cause)) {
25
+ return true;
26
+ }
27
+ return false;
28
+ }
29
+ export function attachRequestLifecycle(req, res, logger, requestId) {
30
+ const controller = new AbortController();
31
+ let alive = true;
32
+ const markGone = (source, err) => {
33
+ if (!alive) {
34
+ return;
35
+ }
36
+ alive = false;
37
+ if (err && !isBenignSocketError(err)) {
38
+ logger.debug({ event: "request_lifecycle_error", request_id: requestId, source, err }, "request stream errored");
39
+ }
40
+ else if (source !== "close" || err) {
41
+ logger.debug({ event: "request_lifecycle_closed", request_id: requestId, source }, "client disconnected");
42
+ }
43
+ if (!controller.signal.aborted) {
44
+ controller.abort();
45
+ }
46
+ };
47
+ res.on("close", () => markGone("close"));
48
+ res.on("error", (err) => markGone("error", err));
49
+ req.on("aborted", () => markGone("aborted"));
50
+ req.on("error", (err) => markGone("error", err));
51
+ return {
52
+ signal: controller.signal,
53
+ isAlive: () => alive && res.writable && !res.writableEnded
54
+ };
55
+ }
56
+ /**
57
+ * Writes a JSON response, but is a no-op when the response is already
58
+ * committed or the socket is gone. This is the safe replacement for
59
+ * `res.setHeader(...) + res.end(...)` in any path that might run after a
60
+ * streaming response has started flushing.
61
+ */
62
+ export function safeSendJson(res, status, payload) {
63
+ if (res.headersSent || !res.writable || res.writableEnded) {
64
+ return;
65
+ }
66
+ try {
67
+ res.statusCode = status;
68
+ res.setHeader("Content-Type", "application/json");
69
+ res.end(JSON.stringify(payload));
70
+ }
71
+ catch (error) {
72
+ if (isBenignSocketError(error)) {
73
+ return;
74
+ }
75
+ throw error;
76
+ }
77
+ }
78
+ /**
79
+ * Best-effort write to a downstream Writable. Returns false when the chunk
80
+ * could not be delivered (because the stream is destroyed or the write
81
+ * threw a benign socket error). Non-benign errors are re-thrown so they're
82
+ * still visible to the caller.
83
+ */
84
+ export function safeWrite(downstream, chunk) {
85
+ if (!downstream.writable || downstream.writableEnded || downstream.destroyed) {
86
+ return false;
87
+ }
88
+ try {
89
+ return downstream.write(chunk);
90
+ }
91
+ catch (error) {
92
+ if (isBenignSocketError(error)) {
93
+ return false;
94
+ }
95
+ throw error;
96
+ }
97
+ }
98
+ /**
99
+ * Best-effort end of a downstream Writable. Swallows benign socket errors;
100
+ * never throws on a destroyed stream.
101
+ */
102
+ export function safeEnd(downstream) {
103
+ if (downstream.writableEnded || downstream.destroyed) {
104
+ return;
105
+ }
106
+ try {
107
+ downstream.end();
108
+ }
109
+ catch (error) {
110
+ if (isBenignSocketError(error)) {
111
+ return;
112
+ }
113
+ throw error;
114
+ }
115
+ }