@socketsecurity/lib 5.13.0 → 5.14.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/CHANGELOG.md +18 -0
- package/dist/http-request.d.ts +136 -2
- package/dist/http-request.js +164 -35
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [5.14.0](https://github.com/SocketDev/socket-lib/releases/tag/v5.14.0) - 2026-04-06
|
|
9
|
+
|
|
10
|
+
### Added — http-request
|
|
11
|
+
|
|
12
|
+
- `HttpResponseError` class — thrown on non-2xx when `throwOnError` is enabled, carries the full `HttpResponse`
|
|
13
|
+
- `throwOnError` option on `HttpRequestOptions` — non-2xx responses throw instead of resolving with `ok: false`, enabling retry of HTTP errors
|
|
14
|
+
- `onRetry` callback on `HttpRequestOptions` — customize retry behavior per-attempt (return `false` to stop, a `number` to override delay, `undefined` for default backoff)
|
|
15
|
+
- Streaming body support — `body` accepts `Readable` streams (incl. `form-data` npm package), auto-merges `getHeaders()` when present
|
|
16
|
+
- `parseRetryAfterHeader()` — standalone RFC 7231 §7.1.3 `Retry-After` header parser (strict integer seconds + HTTP-date formats)
|
|
17
|
+
- `sanitizeHeaders()` — redact sensitive headers (`authorization`, `cookie`, `set-cookie`, `proxy-authorization`, `proxy-authenticate`, `www-authenticate`) for safe logging
|
|
18
|
+
|
|
19
|
+
### Changed — http-request
|
|
20
|
+
|
|
21
|
+
- `HttpRequestOptions.body` type widened from `Buffer | string` to `Buffer | Readable | string`
|
|
22
|
+
- Redirect responses now drained via `res.resume()` to free sockets
|
|
23
|
+
- `maxResponseSize` exceeded now cleans up both response and request
|
|
24
|
+
- `onResponse` hooks wrapped in try/catch — user hook errors can no longer leave promises pending
|
|
25
|
+
|
|
8
26
|
## [5.13.0](https://github.com/SocketDev/socket-lib/releases/tag/v5.13.0) - 2026-04-05
|
|
9
27
|
|
|
10
28
|
### Added — http-request
|
package/dist/http-request.d.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview HTTP/HTTPS request utilities using Node.js built-in modules with retry logic, redirects, and download support.
|
|
3
|
+
*
|
|
4
|
+
* This module provides a fetch-like API built on top of Node.js native `http` and `https` modules.
|
|
5
|
+
* It supports automatic retries with exponential backoff, redirect following, streaming downloads,
|
|
6
|
+
* and provides a familiar fetch-style response interface.
|
|
7
|
+
*
|
|
8
|
+
* Key Features:
|
|
9
|
+
* - Automatic retries with exponential backoff for failed requests.
|
|
10
|
+
* - Redirect following with configurable max redirects.
|
|
11
|
+
* - Streaming downloads with progress callbacks.
|
|
12
|
+
* - Fetch-like response interface (`.json()`, `.text()`, `.arrayBuffer()`).
|
|
13
|
+
* - Timeout support for all operations.
|
|
14
|
+
* - Zero dependencies on external HTTP libraries.
|
|
15
|
+
*/
|
|
16
|
+
import type { Readable } from 'node:stream';
|
|
1
17
|
import type { IncomingHttpHeaders, IncomingMessage } from 'http';
|
|
2
18
|
/** IncomingMessage received as a response to a client request (http.request callback). */
|
|
3
19
|
export type IncomingResponse = IncomingMessage;
|
|
@@ -39,7 +55,17 @@ export interface HttpHooks {
|
|
|
39
55
|
export interface HttpRequestOptions {
|
|
40
56
|
/**
|
|
41
57
|
* Request body to send.
|
|
42
|
-
* Can be a string
|
|
58
|
+
* Can be a string, Buffer, or Readable stream.
|
|
59
|
+
*
|
|
60
|
+
* When a Readable stream is provided, it is piped directly to the request.
|
|
61
|
+
* If the stream has a `getHeaders()` method (duck-typed, e.g., the `form-data`
|
|
62
|
+
* npm package), its headers (Content-Type with boundary) are automatically
|
|
63
|
+
* merged into the request headers.
|
|
64
|
+
*
|
|
65
|
+
* **Note:** Streaming bodies are one-shot — they cannot be replayed. Using a
|
|
66
|
+
* Readable body with `retries > 0` throws an error. Buffer the body as a
|
|
67
|
+
* string/Buffer if retries are needed. Redirects are also disabled for
|
|
68
|
+
* streaming bodies since the stream is consumed on the first request.
|
|
43
69
|
*
|
|
44
70
|
* @example
|
|
45
71
|
* ```ts
|
|
@@ -56,9 +82,18 @@ export interface HttpRequestOptions {
|
|
|
56
82
|
* method: 'POST',
|
|
57
83
|
* body: buffer
|
|
58
84
|
* })
|
|
85
|
+
*
|
|
86
|
+
* // Stream form-data (npm package, not native FormData)
|
|
87
|
+
* import FormData from 'form-data'
|
|
88
|
+
* const form = new FormData()
|
|
89
|
+
* form.append('file', createReadStream('data.json'))
|
|
90
|
+
* await httpRequest('https://api.example.com/upload', {
|
|
91
|
+
* method: 'POST',
|
|
92
|
+
* body: form // auto-merges form.getHeaders()
|
|
93
|
+
* })
|
|
59
94
|
* ```
|
|
60
95
|
*/
|
|
61
|
-
body?: Buffer | string | undefined;
|
|
96
|
+
body?: Buffer | Readable | string | undefined;
|
|
62
97
|
/**
|
|
63
98
|
* Custom CA certificates for TLS connections.
|
|
64
99
|
* When provided, these certificates are combined with the default trust
|
|
@@ -162,6 +197,38 @@ export interface HttpRequestOptions {
|
|
|
162
197
|
* ```
|
|
163
198
|
*/
|
|
164
199
|
method?: string | undefined;
|
|
200
|
+
/**
|
|
201
|
+
* Callback invoked before each retry attempt.
|
|
202
|
+
* Allows customizing retry behavior per-attempt (e.g., skip 4xx, honor Retry-After).
|
|
203
|
+
*
|
|
204
|
+
* @param attempt - Current retry attempt number (1-based)
|
|
205
|
+
* @param error - The error that triggered the retry (HttpResponseError for HTTP errors)
|
|
206
|
+
* @param delay - The calculated delay in ms before next retry
|
|
207
|
+
* @returns `false` to stop retrying and rethrow,
|
|
208
|
+
* a `number` to override the delay (ms),
|
|
209
|
+
* or `undefined` to use the calculated delay
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```ts
|
|
213
|
+
* await httpRequest('https://api.example.com/data', {
|
|
214
|
+
* retries: 3,
|
|
215
|
+
* throwOnError: true,
|
|
216
|
+
* onRetry: (attempt, error, delay) => {
|
|
217
|
+
* // Don't retry client errors (except 429)
|
|
218
|
+
* if (error instanceof HttpResponseError) {
|
|
219
|
+
* if (error.response.status === 429) {
|
|
220
|
+
* const retryAfter = parseRetryAfterHeader(error.response.headers['retry-after'])
|
|
221
|
+
* return retryAfter ?? undefined
|
|
222
|
+
* }
|
|
223
|
+
* if (error.response.status >= 400 && error.response.status < 500) {
|
|
224
|
+
* return false
|
|
225
|
+
* }
|
|
226
|
+
* }
|
|
227
|
+
* }
|
|
228
|
+
* })
|
|
229
|
+
* ```
|
|
230
|
+
*/
|
|
231
|
+
onRetry?: ((attempt: number, error: unknown, delay: number) => boolean | number | undefined) | undefined;
|
|
165
232
|
/**
|
|
166
233
|
* Number of retry attempts for failed requests.
|
|
167
234
|
* Uses exponential backoff: delay = `retryDelay` * 2^attempt.
|
|
@@ -194,6 +261,23 @@ export interface HttpRequestOptions {
|
|
|
194
261
|
* ```
|
|
195
262
|
*/
|
|
196
263
|
retryDelay?: number | undefined;
|
|
264
|
+
/**
|
|
265
|
+
* When true, non-2xx HTTP responses throw an `HttpResponseError` instead
|
|
266
|
+
* of resolving with `response.ok === false`. This makes HTTP error
|
|
267
|
+
* responses eligible for retry via the `retries` option.
|
|
268
|
+
*
|
|
269
|
+
* @default false
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* ```ts
|
|
273
|
+
* // Throw on 4xx/5xx responses (enabling retry for 5xx)
|
|
274
|
+
* await httpRequest('https://api.example.com/data', {
|
|
275
|
+
* throwOnError: true,
|
|
276
|
+
* retries: 3
|
|
277
|
+
* })
|
|
278
|
+
* ```
|
|
279
|
+
*/
|
|
280
|
+
throwOnError?: boolean | undefined;
|
|
197
281
|
/**
|
|
198
282
|
* Request timeout in milliseconds.
|
|
199
283
|
* If the request takes longer than this, it will be aborted.
|
|
@@ -333,6 +417,56 @@ export interface HttpResponse {
|
|
|
333
417
|
* into the standard HttpResponse interface.
|
|
334
418
|
*/
|
|
335
419
|
export declare function readIncomingResponse(msg: IncomingResponse): Promise<HttpResponse>;
|
|
420
|
+
/**
|
|
421
|
+
* Error thrown when an HTTP response has a non-2xx status code
|
|
422
|
+
* and `throwOnError` is enabled. Carries the full `HttpResponse`
|
|
423
|
+
* so callers can inspect status, headers, and body.
|
|
424
|
+
*/
|
|
425
|
+
export declare class HttpResponseError extends Error {
|
|
426
|
+
response: HttpResponse;
|
|
427
|
+
constructor(response: HttpResponse, message?: string | undefined);
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Parse a `Retry-After` HTTP header value into milliseconds.
|
|
431
|
+
*
|
|
432
|
+
* Supports both formats defined in RFC 7231 §7.1.3:
|
|
433
|
+
* - **delay-seconds**: integer number of seconds (e.g., `"120"`)
|
|
434
|
+
* - **HTTP-date**: an absolute date/time (e.g., `"Fri, 31 Dec 2027 23:59:59 GMT"`)
|
|
435
|
+
*
|
|
436
|
+
* When the header is an array (multiple values), the first element is used.
|
|
437
|
+
*
|
|
438
|
+
* @param value - The raw Retry-After header value(s)
|
|
439
|
+
* @returns Delay in milliseconds, or `undefined` if the value cannot be parsed
|
|
440
|
+
*
|
|
441
|
+
* @example
|
|
442
|
+
* ```ts
|
|
443
|
+
* const delay = parseRetryAfterHeader(response.headers['retry-after'])
|
|
444
|
+
* if (delay !== undefined) {
|
|
445
|
+
* await new Promise(resolve => setTimeout(resolve, delay))
|
|
446
|
+
* }
|
|
447
|
+
* ```
|
|
448
|
+
*/
|
|
449
|
+
export declare function parseRetryAfterHeader(value: string | string[] | undefined): number | undefined;
|
|
450
|
+
/**
|
|
451
|
+
* Redact sensitive HTTP headers for safe logging and telemetry.
|
|
452
|
+
*
|
|
453
|
+
* Replaces values of sensitive headers (Authorization, Cookie, etc.)
|
|
454
|
+
* with `[REDACTED]`. Non-sensitive headers are passed through unchanged.
|
|
455
|
+
* Array values are joined with `', '`.
|
|
456
|
+
*
|
|
457
|
+
* @param headers - HTTP headers to sanitize
|
|
458
|
+
* @returns A new object with sensitive values redacted
|
|
459
|
+
*
|
|
460
|
+
* @example
|
|
461
|
+
* ```ts
|
|
462
|
+
* const safe = sanitizeHeaders({
|
|
463
|
+
* 'authorization': 'Bearer secret',
|
|
464
|
+
* 'content-type': 'application/json'
|
|
465
|
+
* })
|
|
466
|
+
* // { authorization: '[REDACTED]', 'content-type': 'application/json' }
|
|
467
|
+
* ```
|
|
468
|
+
*/
|
|
469
|
+
export declare function sanitizeHeaders(headers: Record<string, unknown> | undefined): Record<string, string>;
|
|
336
470
|
/**
|
|
337
471
|
* Configuration options for file downloads.
|
|
338
472
|
*/
|
package/dist/http-request.js
CHANGED
|
@@ -19,6 +19,7 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
19
19
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
20
|
var http_request_exports = {};
|
|
21
21
|
__export(http_request_exports, {
|
|
22
|
+
HttpResponseError: () => HttpResponseError,
|
|
22
23
|
enrichErrorMessage: () => enrichErrorMessage,
|
|
23
24
|
fetchChecksums: () => fetchChecksums,
|
|
24
25
|
httpDownload: () => httpDownload,
|
|
@@ -26,7 +27,9 @@ __export(http_request_exports, {
|
|
|
26
27
|
httpRequest: () => httpRequest,
|
|
27
28
|
httpText: () => httpText,
|
|
28
29
|
parseChecksums: () => parseChecksums,
|
|
29
|
-
|
|
30
|
+
parseRetryAfterHeader: () => parseRetryAfterHeader,
|
|
31
|
+
readIncomingResponse: () => readIncomingResponse,
|
|
32
|
+
sanitizeHeaders: () => sanitizeHeaders
|
|
30
33
|
});
|
|
31
34
|
module.exports = __toCommonJS(http_request_exports);
|
|
32
35
|
var import_fs = require("./fs.js");
|
|
@@ -85,6 +88,64 @@ async function readIncomingResponse(msg) {
|
|
|
85
88
|
text: () => body.toString("utf8")
|
|
86
89
|
};
|
|
87
90
|
}
|
|
91
|
+
class HttpResponseError extends Error {
|
|
92
|
+
response;
|
|
93
|
+
constructor(response, message) {
|
|
94
|
+
const statusCode = response.status ?? "unknown";
|
|
95
|
+
const statusMessage = response.statusText || "No status message";
|
|
96
|
+
super(message ?? `HTTP ${statusCode}: ${statusMessage}`);
|
|
97
|
+
this.name = "HttpResponseError";
|
|
98
|
+
this.response = response;
|
|
99
|
+
Error.captureStackTrace(this, HttpResponseError);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function parseRetryAfterHeader(value) {
|
|
103
|
+
if (!value) {
|
|
104
|
+
return void 0;
|
|
105
|
+
}
|
|
106
|
+
const raw = Array.isArray(value) ? value[0] : value;
|
|
107
|
+
if (!raw) {
|
|
108
|
+
return void 0;
|
|
109
|
+
}
|
|
110
|
+
const trimmed = raw.trim();
|
|
111
|
+
if (/^\d+$/.test(trimmed)) {
|
|
112
|
+
const seconds = Number(trimmed);
|
|
113
|
+
return seconds * 1e3;
|
|
114
|
+
}
|
|
115
|
+
const date = new Date(raw);
|
|
116
|
+
if (!Number.isNaN(date.getTime())) {
|
|
117
|
+
const delayMs = date.getTime() - Date.now();
|
|
118
|
+
if (delayMs > 0) {
|
|
119
|
+
return delayMs;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return void 0;
|
|
123
|
+
}
|
|
124
|
+
function sanitizeHeaders(headers) {
|
|
125
|
+
if (!headers) {
|
|
126
|
+
return {};
|
|
127
|
+
}
|
|
128
|
+
const sensitiveHeaders = /* @__PURE__ */ new Set([
|
|
129
|
+
"authorization",
|
|
130
|
+
"cookie",
|
|
131
|
+
"proxy-authorization",
|
|
132
|
+
"proxy-authenticate",
|
|
133
|
+
"set-cookie",
|
|
134
|
+
"www-authenticate"
|
|
135
|
+
]);
|
|
136
|
+
const result = { __proto__: null };
|
|
137
|
+
for (const key of Object.keys(headers)) {
|
|
138
|
+
const value = headers[key];
|
|
139
|
+
if (sensitiveHeaders.has(key.toLowerCase())) {
|
|
140
|
+
result[key] = "[REDACTED]";
|
|
141
|
+
} else if (Array.isArray(value)) {
|
|
142
|
+
result[key] = value.join(", ");
|
|
143
|
+
} else if (value !== void 0 && value !== null) {
|
|
144
|
+
result[key] = String(value);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
88
149
|
function parseChecksums(text) {
|
|
89
150
|
const checksums = { __proto__: null };
|
|
90
151
|
for (const line of text.split("\n")) {
|
|
@@ -297,12 +358,34 @@ async function httpRequestAttempt(url, options) {
|
|
|
297
358
|
timeout = 3e4
|
|
298
359
|
} = { __proto__: null, ...options };
|
|
299
360
|
const startTime = Date.now();
|
|
361
|
+
const streamHeaders = body && typeof body === "object" && "getHeaders" in body && typeof body.getHeaders === "function" ? body.getHeaders() : void 0;
|
|
300
362
|
const mergedHeaders = {
|
|
301
363
|
"User-Agent": "socket-registry/1.0",
|
|
364
|
+
...streamHeaders,
|
|
302
365
|
...headers
|
|
303
366
|
};
|
|
304
367
|
hooks?.onRequest?.({ method, url, headers: mergedHeaders, timeout });
|
|
305
368
|
return await new Promise((resolve, reject) => {
|
|
369
|
+
let settled = false;
|
|
370
|
+
const resolveOnce = (response) => {
|
|
371
|
+
if (settled) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
settled = true;
|
|
375
|
+
resolve(response);
|
|
376
|
+
};
|
|
377
|
+
const rejectOnce = (err) => {
|
|
378
|
+
if (settled) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
settled = true;
|
|
382
|
+
if (body && typeof body === "object" && typeof body.destroy === "function") {
|
|
383
|
+
;
|
|
384
|
+
body.destroy();
|
|
385
|
+
}
|
|
386
|
+
emitResponse({ error: err });
|
|
387
|
+
reject(err);
|
|
388
|
+
};
|
|
306
389
|
const parsedUrl = new URL(url);
|
|
307
390
|
const isHttps = parsedUrl.protocol === "https:";
|
|
308
391
|
const httpModule = isHttps ? /* @__PURE__ */ getHttps() : /* @__PURE__ */ getHttp();
|
|
@@ -318,23 +401,28 @@ async function httpRequestAttempt(url, options) {
|
|
|
318
401
|
requestOptions["ca"] = ca;
|
|
319
402
|
}
|
|
320
403
|
const emitResponse = (info) => {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
404
|
+
try {
|
|
405
|
+
hooks?.onResponse?.({
|
|
406
|
+
duration: Date.now() - startTime,
|
|
407
|
+
method,
|
|
408
|
+
url,
|
|
409
|
+
...info
|
|
410
|
+
});
|
|
411
|
+
} catch {
|
|
412
|
+
}
|
|
327
413
|
};
|
|
328
414
|
const request = httpModule.request(
|
|
329
415
|
requestOptions,
|
|
330
416
|
(res) => {
|
|
331
417
|
if (followRedirects && res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
418
|
+
res.resume();
|
|
332
419
|
emitResponse({
|
|
333
420
|
headers: res.headers,
|
|
334
421
|
status: res.statusCode,
|
|
335
422
|
statusText: res.statusMessage
|
|
336
423
|
});
|
|
337
424
|
if (maxRedirects <= 0) {
|
|
425
|
+
settled = true;
|
|
338
426
|
reject(
|
|
339
427
|
new Error(
|
|
340
428
|
`Too many redirects (exceeded maximum: ${maxRedirects})`
|
|
@@ -345,6 +433,7 @@ async function httpRequestAttempt(url, options) {
|
|
|
345
433
|
const redirectUrl = res.headers.location.startsWith("http") ? res.headers.location : new URL(res.headers.location, url).toString();
|
|
346
434
|
const redirectParsed = new URL(redirectUrl);
|
|
347
435
|
if (isHttps && redirectParsed.protocol !== "https:") {
|
|
436
|
+
settled = true;
|
|
348
437
|
reject(
|
|
349
438
|
new Error(
|
|
350
439
|
`Redirect from HTTPS to HTTP is not allowed: ${redirectUrl}`
|
|
@@ -352,6 +441,7 @@ async function httpRequestAttempt(url, options) {
|
|
|
352
441
|
);
|
|
353
442
|
return;
|
|
354
443
|
}
|
|
444
|
+
settled = true;
|
|
355
445
|
resolve(
|
|
356
446
|
httpRequestAttempt(redirectUrl, {
|
|
357
447
|
body,
|
|
@@ -373,18 +463,22 @@ async function httpRequestAttempt(url, options) {
|
|
|
373
463
|
totalBytes += chunk.length;
|
|
374
464
|
if (maxResponseSize && totalBytes > maxResponseSize) {
|
|
375
465
|
res.destroy();
|
|
466
|
+
request.destroy();
|
|
376
467
|
const sizeMB = (totalBytes / (1024 * 1024)).toFixed(2);
|
|
377
468
|
const maxMB = (maxResponseSize / (1024 * 1024)).toFixed(2);
|
|
378
|
-
|
|
379
|
-
|
|
469
|
+
rejectOnce(
|
|
470
|
+
new Error(
|
|
471
|
+
`Response exceeds maximum size limit (${sizeMB}MB > ${maxMB}MB)`
|
|
472
|
+
)
|
|
380
473
|
);
|
|
381
|
-
emitResponse({ error: err });
|
|
382
|
-
reject(err);
|
|
383
474
|
return;
|
|
384
475
|
}
|
|
385
476
|
chunks.push(chunk);
|
|
386
477
|
});
|
|
387
478
|
res.on("end", () => {
|
|
479
|
+
if (settled) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
388
482
|
const responseBody = Buffer.concat(chunks);
|
|
389
483
|
const ok = res.statusCode !== void 0 && res.statusCode >= 200 && res.statusCode < 300;
|
|
390
484
|
const response = {
|
|
@@ -412,11 +506,10 @@ async function httpRequestAttempt(url, options) {
|
|
|
412
506
|
status: res.statusCode,
|
|
413
507
|
statusText: res.statusMessage
|
|
414
508
|
});
|
|
415
|
-
|
|
509
|
+
resolveOnce(response);
|
|
416
510
|
});
|
|
417
511
|
res.on("error", (error) => {
|
|
418
|
-
|
|
419
|
-
reject(error);
|
|
512
|
+
rejectOnce(error);
|
|
420
513
|
});
|
|
421
514
|
}
|
|
422
515
|
);
|
|
@@ -426,24 +519,33 @@ async function httpRequestAttempt(url, options) {
|
|
|
426
519
|
method,
|
|
427
520
|
error
|
|
428
521
|
);
|
|
429
|
-
|
|
430
|
-
emitResponse({ error: enhanced });
|
|
431
|
-
reject(enhanced);
|
|
522
|
+
rejectOnce(new Error(message, { cause: error }));
|
|
432
523
|
});
|
|
433
524
|
request.on("timeout", () => {
|
|
434
525
|
request.destroy();
|
|
435
|
-
|
|
436
|
-
|
|
526
|
+
rejectOnce(
|
|
527
|
+
new Error(
|
|
528
|
+
`${method} request timed out after ${timeout}ms: ${url}
|
|
437
529
|
\u2192 Server did not respond in time.
|
|
438
530
|
\u2192 Try: Increase timeout or check network connectivity.`
|
|
531
|
+
)
|
|
439
532
|
);
|
|
440
|
-
emitResponse({ error: err });
|
|
441
|
-
reject(err);
|
|
442
533
|
});
|
|
443
534
|
if (body) {
|
|
535
|
+
if (typeof body === "object" && typeof body.pipe === "function") {
|
|
536
|
+
const stream = body;
|
|
537
|
+
stream.on("error", (err) => {
|
|
538
|
+
request.destroy();
|
|
539
|
+
rejectOnce(err);
|
|
540
|
+
});
|
|
541
|
+
stream.pipe(request);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
444
544
|
request.write(body);
|
|
545
|
+
request.end();
|
|
546
|
+
} else {
|
|
547
|
+
request.end();
|
|
445
548
|
}
|
|
446
|
-
request.end();
|
|
447
549
|
});
|
|
448
550
|
}
|
|
449
551
|
async function httpDownload(url, destPath, options) {
|
|
@@ -571,31 +673,55 @@ async function httpRequest(url, options) {
|
|
|
571
673
|
maxRedirects = 5,
|
|
572
674
|
maxResponseSize,
|
|
573
675
|
method = "GET",
|
|
676
|
+
onRetry,
|
|
574
677
|
retries = 0,
|
|
575
678
|
retryDelay = 1e3,
|
|
679
|
+
throwOnError = false,
|
|
576
680
|
timeout = 3e4
|
|
577
681
|
} = { __proto__: null, ...options };
|
|
682
|
+
const isStreamBody = body !== void 0 && typeof body === "object" && typeof body.pipe === "function";
|
|
683
|
+
if (isStreamBody && retries > 0) {
|
|
684
|
+
throw new Error(
|
|
685
|
+
"Streaming body (Readable/FormData) cannot be used with retries. Streams are consumed on first attempt and cannot be replayed. Set retries: 0 or buffer the body as a string/Buffer."
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
const attemptOpts = {
|
|
689
|
+
body,
|
|
690
|
+
ca,
|
|
691
|
+
// Disable redirect following for stream bodies — the stream is consumed
|
|
692
|
+
// on the first request and cannot be re-piped to the redirect target.
|
|
693
|
+
followRedirects: isStreamBody ? false : followRedirects,
|
|
694
|
+
headers,
|
|
695
|
+
hooks,
|
|
696
|
+
maxRedirects,
|
|
697
|
+
maxResponseSize,
|
|
698
|
+
method,
|
|
699
|
+
timeout
|
|
700
|
+
};
|
|
578
701
|
let lastError;
|
|
579
702
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
580
703
|
try {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
hooks,
|
|
587
|
-
maxRedirects,
|
|
588
|
-
maxResponseSize,
|
|
589
|
-
method,
|
|
590
|
-
timeout
|
|
591
|
-
});
|
|
704
|
+
const response = await httpRequestAttempt(url, attemptOpts);
|
|
705
|
+
if (throwOnError && !response.ok) {
|
|
706
|
+
throw new HttpResponseError(response);
|
|
707
|
+
}
|
|
708
|
+
return response;
|
|
592
709
|
} catch (e) {
|
|
593
710
|
lastError = e;
|
|
594
711
|
if (attempt === retries) {
|
|
595
712
|
break;
|
|
596
713
|
}
|
|
597
714
|
const delayMs = retryDelay * 2 ** attempt;
|
|
598
|
-
|
|
715
|
+
if (onRetry) {
|
|
716
|
+
const retryResult = onRetry(attempt + 1, e, delayMs);
|
|
717
|
+
if (retryResult === false) {
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
720
|
+
const actualDelay = typeof retryResult === "number" && !Number.isNaN(retryResult) ? Math.max(0, retryResult) : delayMs;
|
|
721
|
+
await new Promise((resolve) => setTimeout(resolve, actualDelay));
|
|
722
|
+
} else {
|
|
723
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
724
|
+
}
|
|
599
725
|
}
|
|
600
726
|
}
|
|
601
727
|
throw lastError || new Error("Request failed after retries");
|
|
@@ -631,6 +757,7 @@ async function httpText(url, options) {
|
|
|
631
757
|
}
|
|
632
758
|
// Annotate the CommonJS export names for ESM import in node:
|
|
633
759
|
0 && (module.exports = {
|
|
760
|
+
HttpResponseError,
|
|
634
761
|
enrichErrorMessage,
|
|
635
762
|
fetchChecksums,
|
|
636
763
|
httpDownload,
|
|
@@ -638,5 +765,7 @@ async function httpText(url, options) {
|
|
|
638
765
|
httpRequest,
|
|
639
766
|
httpText,
|
|
640
767
|
parseChecksums,
|
|
641
|
-
|
|
768
|
+
parseRetryAfterHeader,
|
|
769
|
+
readIncomingResponse,
|
|
770
|
+
sanitizeHeaders
|
|
642
771
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@socketsecurity/lib",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.14.0",
|
|
4
4
|
"packageManager": "pnpm@10.33.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Core utilities and infrastructure for Socket.dev security tools",
|
|
@@ -735,7 +735,7 @@
|
|
|
735
735
|
"@socketregistry/is-unicode-supported": "1.0.5",
|
|
736
736
|
"@socketregistry/packageurl-js": "1.4.1",
|
|
737
737
|
"@socketregistry/yocto-spinner": "1.0.25",
|
|
738
|
-
"@socketsecurity/lib-stable": "npm:@socketsecurity/lib@5.
|
|
738
|
+
"@socketsecurity/lib-stable": "npm:@socketsecurity/lib@5.13.0",
|
|
739
739
|
"@types/node": "24.9.2",
|
|
740
740
|
"@typescript/native-preview": "7.0.0-dev.20250920.1",
|
|
741
741
|
"@vitest/coverage-v8": "4.0.3",
|