express-api-stress-tester 2.0.3 → 2.0.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "express-api-stress-tester",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "High-performance distributed API stress testing platform for Express.js APIs — simulate up to 10M concurrent virtual users",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -4,7 +4,31 @@
4
4
  * Provides keep-alive, pipelining, and precise response-time tracking
5
5
  * via process.hrtime.bigint().
6
6
  */
7
- import { Pool } from 'undici';
7
+ async function ensureWebStreamsGlobals() {
8
+ if (typeof globalThis.ReadableStream !== 'undefined') return;
9
+ try {
10
+ const web = await import('node:stream/web');
11
+ if (web.ReadableStream && typeof globalThis.ReadableStream === 'undefined') {
12
+ globalThis.ReadableStream = web.ReadableStream;
13
+ }
14
+ if (web.WritableStream && typeof globalThis.WritableStream === 'undefined') {
15
+ globalThis.WritableStream = web.WritableStream;
16
+ }
17
+ if (web.TransformStream && typeof globalThis.TransformStream === 'undefined') {
18
+ globalThis.TransformStream = web.TransformStream;
19
+ }
20
+ } catch {
21
+ // ignore
22
+ }
23
+ }
24
+
25
+ let cachedUndici = null;
26
+ async function getUndici() {
27
+ if (cachedUndici) return cachedUndici;
28
+ await ensureWebStreamsGlobals();
29
+ cachedUndici = await import('undici');
30
+ return cachedUndici;
31
+ }
8
32
 
9
33
  const CONTROL_CHARS_REGEX = /[\0\r\n]/g;
10
34
  const MAX_WARNED_HEADER_VALUES = 100;
@@ -36,14 +60,28 @@ export class HttpEngine {
36
60
  this.timeout = timeout;
37
61
  this.invalidHeaderWarningCache = { map: new Map(), queue: [] };
38
62
 
39
- this.pool = new Pool(baseUrl, {
63
+ this.pool = null;
64
+ this.poolPromise = null;
65
+ this.poolOptions = {
40
66
  connections,
41
67
  pipelining,
42
68
  keepAliveTimeout: 30_000,
43
69
  keepAliveMaxTimeout: 60_000,
44
70
  headersTimeout: timeout,
45
71
  bodyTimeout: timeout,
46
- });
72
+ };
73
+ }
74
+
75
+ async ensurePool() {
76
+ if (this.pool) return this.pool;
77
+ if (!this.poolPromise) {
78
+ this.poolPromise = (async () => {
79
+ const { Pool } = await getUndici();
80
+ this.pool = new Pool(this.baseUrl, this.poolOptions);
81
+ return this.pool;
82
+ })();
83
+ }
84
+ return this.poolPromise;
47
85
  }
48
86
 
49
87
  /**
@@ -57,6 +95,7 @@ export class HttpEngine {
57
95
  * @returns {Promise<{ statusCode: number, headers: object, body: string, responseTime: number }>}
58
96
  */
59
97
  async request({ method = 'GET', path = '/', headers = {}, body = null } = {}) {
98
+ const pool = await this.ensurePool();
60
99
  const mergedHeaders = normalizeHeaders(
61
100
  { ...this.defaultHeaders, ...headers },
62
101
  this.invalidHeaderWarningCache,
@@ -64,7 +103,7 @@ export class HttpEngine {
64
103
 
65
104
  const start = process.hrtime.bigint();
66
105
 
67
- const { statusCode, headers: resHeaders, body: resBody } = await this.pool.request({
106
+ const { statusCode, headers: resHeaders, body: resBody } = await pool.request({
68
107
  method: method.toUpperCase(),
69
108
  path,
70
109
  headers: mergedHeaders,
@@ -74,7 +113,16 @@ export class HttpEngine {
74
113
  });
75
114
 
76
115
  // Consume the body fully (undici requirement to free the socket)
77
- const text = await resBody.text();
116
+ // Drain via async iteration to avoid depending on Web Streams globals.
117
+ if (resBody) {
118
+ try {
119
+ for await (const _chunk of resBody) {
120
+ // discard
121
+ }
122
+ } catch {
123
+ // ignore body drain errors
124
+ }
125
+ }
78
126
 
79
127
  const end = process.hrtime.bigint();
80
128
  // Convert nanoseconds → milliseconds (floating point)
@@ -83,7 +131,7 @@ export class HttpEngine {
83
131
  return {
84
132
  statusCode,
85
133
  headers: resHeaders,
86
- body: text,
134
+ body: '',
87
135
  responseTime,
88
136
  };
89
137
  }
@@ -92,7 +140,16 @@ export class HttpEngine {
92
140
  * Gracefully close the connection pool.
93
141
  */
94
142
  async close() {
95
- await this.pool.close();
143
+ if (this.pool) {
144
+ await this.pool.close();
145
+ return;
146
+ }
147
+ if (this.poolPromise) {
148
+ const pool = await this.poolPromise;
149
+ if (pool && typeof pool.close === 'function') {
150
+ await pool.close();
151
+ }
152
+ }
96
153
  }
97
154
  }
98
155
 
@@ -94,6 +94,24 @@ let maxLatency = -Infinity;
94
94
  const statusCodes = {};
95
95
  const perEndpoint = {};
96
96
 
97
+ function normalizeBaseForRelativeUrlResolution(base, maybeRelativePath) {
98
+ if (!base || !maybeRelativePath) return base;
99
+ if (maybeRelativePath.startsWith('http://') || maybeRelativePath.startsWith('https://')) return base;
100
+ if (maybeRelativePath.startsWith('/')) return base;
101
+
102
+ try {
103
+ const parsedBase = new URL(base);
104
+ if (parsedBase.pathname && !parsedBase.pathname.endsWith('/')) {
105
+ parsedBase.pathname = `${parsedBase.pathname}/`;
106
+ return parsedBase.toString();
107
+ }
108
+ } catch {
109
+ // ignore
110
+ }
111
+
112
+ return base;
113
+ }
114
+
97
115
  /**
98
116
  * Resolve the full URL for a route.
99
117
  */
@@ -102,7 +120,8 @@ function resolveUrl(route) {
102
120
  const path = route.path || route.url || '';
103
121
  try {
104
122
  if (path) {
105
- return new URL(path, base).toString();
123
+ const normalizedBase = normalizeBaseForRelativeUrlResolution(base, path);
124
+ return new URL(path, normalizedBase).toString();
106
125
  }
107
126
  if (base) {
108
127
  return new URL(base).toString();
@@ -326,7 +345,9 @@ async function executeRequest(task) {
326
345
  parsed = new URL(targetUrl);
327
346
  } catch {
328
347
  try {
329
- parsed = new URL(targetUrl, config.baseUrl || config.url);
348
+ const base = config.baseUrl || config.url;
349
+ const normalizedBase = normalizeBaseForRelativeUrlResolution(base, targetUrl);
350
+ parsed = new URL(targetUrl, normalizedBase);
330
351
  } catch (err) {
331
352
  throw new Error(
332
353
  `Failed to resolve URL "${targetUrl}" with base "${config.baseUrl || config.url}": ${err.message}`,