@jaredwray/mockhttp 2.0.0 → 3.0.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/README.md +68 -68
- package/dist/index.d.mts +130 -159
- package/dist/index.mjs +200 -231
- package/package.json +21 -18
- package/site/dist/api/index.html +5 -5
- package/site/dist/api/swagger.json +10 -10
- package/site/dist/changelog/index.html +49 -33
- package/site/dist/changelog/page/2/index.html +33 -32
- package/site/dist/changelog/{v0-2-0 → v2-1-0}/index.html +47 -8
- package/site/dist/changelog/{v0-1-0 → v3-0-0}/index.html +71 -22
- package/site/dist/changelog-latest.json +26 -26
- package/site/dist/changelog.json +26 -26
- package/site/dist/docs/bins/index.html +78 -53
- package/site/dist/docs/configuration/index.html +42 -4
- package/site/dist/docs/hosted/index.html +47 -8
- package/site/dist/docs/how-to-deploy/cloudflare/index.html +650 -0
- package/site/dist/docs/how-to-deploy/docker/index.html +664 -0
- package/site/dist/docs/how-to-deploy/wasmer/index.html +625 -0
- package/site/dist/docs/https/index.html +42 -4
- package/site/dist/docs/index.html +44 -5
- package/site/dist/docs/library/index.html +58 -24
- package/site/dist/docs/taps/index.html +42 -4
- package/site/dist/feed.xml +1 -1
- package/site/dist/index.html +44 -5
- package/site/dist/llms-full.txt +390 -115
- package/site/dist/llms.txt +6 -3
- package/site/dist/search-index.json +1 -1
- package/site/dist/sitemap.xml +1 -1
package/dist/index.mjs
CHANGED
|
@@ -13,195 +13,10 @@ import Fastify from "fastify";
|
|
|
13
13
|
import { Hookified } from "hookified";
|
|
14
14
|
import * as crypto$1 from "node:crypto";
|
|
15
15
|
import crypto, { randomBytes, randomInt, randomUUID, timingSafeEqual } from "node:crypto";
|
|
16
|
+
import Keyv from "keyv";
|
|
16
17
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
17
18
|
import { brotliCompressSync, deflateSync, gzipSync } from "node:zlib";
|
|
18
19
|
import { escape } from "html-escaper";
|
|
19
|
-
//#region src/bin-store.ts
|
|
20
|
-
/**
|
|
21
|
-
* In-memory implementation of {@link BinStore} backed by two Maps. State is
|
|
22
|
-
* lost when the process exits.
|
|
23
|
-
*/
|
|
24
|
-
var InMemoryBinStore = class {
|
|
25
|
-
_bins = /* @__PURE__ */ new Map();
|
|
26
|
-
_requests = /* @__PURE__ */ new Map();
|
|
27
|
-
createBin(bin) {
|
|
28
|
-
this._bins.set(bin.id, bin);
|
|
29
|
-
this._requests.set(bin.id, []);
|
|
30
|
-
}
|
|
31
|
-
getBin(id) {
|
|
32
|
-
return this._bins.get(id);
|
|
33
|
-
}
|
|
34
|
-
listBins() {
|
|
35
|
-
return [...this._bins.values()];
|
|
36
|
-
}
|
|
37
|
-
deleteBin(id) {
|
|
38
|
-
this._requests.delete(id);
|
|
39
|
-
return this._bins.delete(id);
|
|
40
|
-
}
|
|
41
|
-
addRequest(binId, req, max) {
|
|
42
|
-
const bin = this._bins.get(binId);
|
|
43
|
-
const list = this._requests.get(binId);
|
|
44
|
-
if (!bin || !list) return;
|
|
45
|
-
list.unshift(req);
|
|
46
|
-
while (list.length > max && list.length > 0) list.pop();
|
|
47
|
-
bin.requestCount = list.length;
|
|
48
|
-
}
|
|
49
|
-
getRequests(binId) {
|
|
50
|
-
return [...this._requests.get(binId) ?? []];
|
|
51
|
-
}
|
|
52
|
-
getRequest(binId, reqId) {
|
|
53
|
-
return this._requests.get(binId)?.find((r) => r.id === reqId);
|
|
54
|
-
}
|
|
55
|
-
clearRequests(binId) {
|
|
56
|
-
const bin = this._bins.get(binId);
|
|
57
|
-
if (!bin) return;
|
|
58
|
-
this._requests.set(binId, []);
|
|
59
|
-
bin.requestCount = 0;
|
|
60
|
-
}
|
|
61
|
-
cleanupExpired(now) {
|
|
62
|
-
const removed = [];
|
|
63
|
-
for (const [id, bin] of this._bins) if (Date.parse(bin.expiresAt) <= now) {
|
|
64
|
-
this._bins.delete(id);
|
|
65
|
-
this._requests.delete(id);
|
|
66
|
-
removed.push(id);
|
|
67
|
-
}
|
|
68
|
-
return removed;
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
//#endregion
|
|
72
|
-
//#region src/bin-manager.ts
|
|
73
|
-
const DEFAULT_TTL_MS = 864e5;
|
|
74
|
-
const DEFAULT_MAX_REQUESTS = 100;
|
|
75
|
-
const DEFAULT_MAX_BODY_SIZE = 1048576;
|
|
76
|
-
const DEFAULT_ID_LENGTH = 12;
|
|
77
|
-
const DEFAULT_CLEANUP_INTERVAL_MS = 6e4;
|
|
78
|
-
/**
|
|
79
|
-
* Manages request bins: ephemeral URL endpoints that capture incoming HTTP
|
|
80
|
-
* requests so they can be inspected later.
|
|
81
|
-
*/
|
|
82
|
-
var BinManager = class {
|
|
83
|
-
_store;
|
|
84
|
-
_defaultTtlMs;
|
|
85
|
-
_maxRequestsPerBin;
|
|
86
|
-
_maxBodySize;
|
|
87
|
-
_idLength;
|
|
88
|
-
_cleanupIntervalMs;
|
|
89
|
-
_cleanupTimer;
|
|
90
|
-
constructor(options) {
|
|
91
|
-
this._store = options?.store ?? new InMemoryBinStore();
|
|
92
|
-
this._defaultTtlMs = options?.defaultTtlMs ?? DEFAULT_TTL_MS;
|
|
93
|
-
this._maxRequestsPerBin = Math.max(0, options?.maxRequestsPerBin ?? DEFAULT_MAX_REQUESTS);
|
|
94
|
-
this._maxBodySize = options?.maxBodySize ?? DEFAULT_MAX_BODY_SIZE;
|
|
95
|
-
this._idLength = options?.idLength ?? DEFAULT_ID_LENGTH;
|
|
96
|
-
this._cleanupIntervalMs = options?.cleanupIntervalMs ?? DEFAULT_CLEANUP_INTERVAL_MS;
|
|
97
|
-
}
|
|
98
|
-
/** Underlying storage backend. */
|
|
99
|
-
get store() {
|
|
100
|
-
return this._store;
|
|
101
|
-
}
|
|
102
|
-
/** Maximum captured body size in bytes. */
|
|
103
|
-
get maxBodySize() {
|
|
104
|
-
return this._maxBodySize;
|
|
105
|
-
}
|
|
106
|
-
/** Maximum number of requests retained per bin. */
|
|
107
|
-
get maxRequestsPerBin() {
|
|
108
|
-
return this._maxRequestsPerBin;
|
|
109
|
-
}
|
|
110
|
-
/** Default bin lifetime in milliseconds. */
|
|
111
|
-
get defaultTtlMs() {
|
|
112
|
-
return this._defaultTtlMs;
|
|
113
|
-
}
|
|
114
|
-
/** Create a new bin with the configured TTL. */
|
|
115
|
-
createBin() {
|
|
116
|
-
const id = this.generateId();
|
|
117
|
-
const now = Date.now();
|
|
118
|
-
const bin = {
|
|
119
|
-
id,
|
|
120
|
-
createdAt: new Date(now).toISOString(),
|
|
121
|
-
expiresAt: new Date(now + this._defaultTtlMs).toISOString(),
|
|
122
|
-
requestCount: 0
|
|
123
|
-
};
|
|
124
|
-
this._store.createBin(bin);
|
|
125
|
-
return bin;
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Look up a bin by id. Returns `undefined` if the bin does not exist or
|
|
129
|
-
* has expired (in which case it is also lazily removed).
|
|
130
|
-
*/
|
|
131
|
-
getBin(id) {
|
|
132
|
-
const bin = this._store.getBin(id);
|
|
133
|
-
if (!bin) return;
|
|
134
|
-
if (Date.parse(bin.expiresAt) <= Date.now()) {
|
|
135
|
-
this._store.deleteBin(id);
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
return bin;
|
|
139
|
-
}
|
|
140
|
-
/** List all non-expired bins. */
|
|
141
|
-
listBins() {
|
|
142
|
-
const now = Date.now();
|
|
143
|
-
const result = [];
|
|
144
|
-
for (const bin of this._store.listBins()) {
|
|
145
|
-
if (Date.parse(bin.expiresAt) <= now) {
|
|
146
|
-
this._store.deleteBin(bin.id);
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
result.push(bin);
|
|
150
|
-
}
|
|
151
|
-
return result;
|
|
152
|
-
}
|
|
153
|
-
/** Delete a bin. Returns true if the bin existed. */
|
|
154
|
-
deleteBin(id) {
|
|
155
|
-
return this._store.deleteBin(id);
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Record an incoming request against a bin. Returns the stored
|
|
159
|
-
* {@link CapturedRequest}, or `undefined` if the bin does not exist or is
|
|
160
|
-
* expired.
|
|
161
|
-
*/
|
|
162
|
-
recordRequest(binId, raw) {
|
|
163
|
-
if (!this.getBin(binId)) return;
|
|
164
|
-
const captured = {
|
|
165
|
-
...raw,
|
|
166
|
-
id: this.generateId(),
|
|
167
|
-
binId,
|
|
168
|
-
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
169
|
-
};
|
|
170
|
-
this._store.addRequest(binId, captured, this._maxRequestsPerBin);
|
|
171
|
-
return captured;
|
|
172
|
-
}
|
|
173
|
-
/** List captured requests for a bin (newest first). */
|
|
174
|
-
getRequests(binId) {
|
|
175
|
-
return this._store.getRequests(binId);
|
|
176
|
-
}
|
|
177
|
-
/** Look up a single captured request. */
|
|
178
|
-
getRequest(binId, reqId) {
|
|
179
|
-
return this._store.getRequest(binId, reqId);
|
|
180
|
-
}
|
|
181
|
-
/** Clear all captured requests in a bin. */
|
|
182
|
-
clearRequests(binId) {
|
|
183
|
-
this._store.clearRequests(binId);
|
|
184
|
-
}
|
|
185
|
-
/** Start the periodic cleanup of expired bins. */
|
|
186
|
-
start() {
|
|
187
|
-
if (this._cleanupTimer) return;
|
|
188
|
-
this._cleanupTimer = setInterval(() => {
|
|
189
|
-
this._store.cleanupExpired(Date.now());
|
|
190
|
-
}, this._cleanupIntervalMs);
|
|
191
|
-
this._cleanupTimer.unref?.();
|
|
192
|
-
}
|
|
193
|
-
/** Stop the periodic cleanup. Idempotent. */
|
|
194
|
-
stop() {
|
|
195
|
-
if (this._cleanupTimer) {
|
|
196
|
-
clearInterval(this._cleanupTimer);
|
|
197
|
-
this._cleanupTimer = void 0;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
generateId() {
|
|
201
|
-
return randomUUID().replaceAll("-", "").slice(0, this._idLength);
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
//#endregion
|
|
205
20
|
//#region src/certificate.ts
|
|
206
21
|
function encodeLength(length) {
|
|
207
22
|
if (length < 128) return Buffer.from([length]);
|
|
@@ -478,6 +293,181 @@ function getFastifyConfig(logging = true) {
|
|
|
478
293
|
};
|
|
479
294
|
}
|
|
480
295
|
//#endregion
|
|
296
|
+
//#region src/mock-http-bins.ts
|
|
297
|
+
const DEFAULT_TTL_MS = 864e5;
|
|
298
|
+
const DEFAULT_MAX_REQUESTS = 100;
|
|
299
|
+
const DEFAULT_MAX_BODY_SIZE = 131072;
|
|
300
|
+
const DEFAULT_ID_LENGTH = 12;
|
|
301
|
+
const BIN_KEY_PREFIX = "bin:";
|
|
302
|
+
/**
|
|
303
|
+
* Stores request bins in Keyv.
|
|
304
|
+
* The default store is `new Keyv()`, which keeps data in memory.
|
|
305
|
+
*/
|
|
306
|
+
var MockHttpBins = class {
|
|
307
|
+
_keyv;
|
|
308
|
+
_defaultTtlMs;
|
|
309
|
+
_maxRequestsPerBin;
|
|
310
|
+
_maxBodySize;
|
|
311
|
+
_idLength;
|
|
312
|
+
_locks = /* @__PURE__ */ new Map();
|
|
313
|
+
constructor(options) {
|
|
314
|
+
this._keyv = options?.keyv ?? new Keyv();
|
|
315
|
+
this._defaultTtlMs = options?.defaultTtlMs ?? DEFAULT_TTL_MS;
|
|
316
|
+
this._maxRequestsPerBin = Math.max(0, options?.maxRequestsPerBin ?? DEFAULT_MAX_REQUESTS);
|
|
317
|
+
this._maxBodySize = options?.maxBodySize ?? DEFAULT_MAX_BODY_SIZE;
|
|
318
|
+
this._idLength = options?.idLength ?? DEFAULT_ID_LENGTH;
|
|
319
|
+
}
|
|
320
|
+
/** Keyv instance that stores the bins. */
|
|
321
|
+
get keyv() {
|
|
322
|
+
return this._keyv;
|
|
323
|
+
}
|
|
324
|
+
/** Maximum captured body size in bytes. */
|
|
325
|
+
get maxBodySize() {
|
|
326
|
+
return this._maxBodySize;
|
|
327
|
+
}
|
|
328
|
+
/** Maximum number of requests in one bin. */
|
|
329
|
+
get maxRequestsPerBin() {
|
|
330
|
+
return this._maxRequestsPerBin;
|
|
331
|
+
}
|
|
332
|
+
/** Default bin lifetime in milliseconds. */
|
|
333
|
+
get defaultTtlMs() {
|
|
334
|
+
return this._defaultTtlMs;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Create a new bin with the configured lifetime.
|
|
338
|
+
* Throws when Keyv does not store the bin.
|
|
339
|
+
*/
|
|
340
|
+
async createBin() {
|
|
341
|
+
const id = this.generateId();
|
|
342
|
+
const now = Date.now();
|
|
343
|
+
const bin = {
|
|
344
|
+
id,
|
|
345
|
+
createdAt: new Date(now).toISOString(),
|
|
346
|
+
expiresAt: new Date(now + this._defaultTtlMs).toISOString(),
|
|
347
|
+
requestCount: 0
|
|
348
|
+
};
|
|
349
|
+
await this.save({
|
|
350
|
+
bin,
|
|
351
|
+
requests: []
|
|
352
|
+
});
|
|
353
|
+
return bin;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Look up a bin by id.
|
|
357
|
+
* Returns `undefined` when the bin does not exist or Keyv has expired it.
|
|
358
|
+
* A failed read also returns `undefined` when the Keyv instance has an `error` listener.
|
|
359
|
+
*/
|
|
360
|
+
async getBin(id) {
|
|
361
|
+
return (await this._keyv.get(this.binKey(id)))?.bin;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* List the bins that Keyv still holds.
|
|
365
|
+
* Throws when the Keyv store has no iterator.
|
|
366
|
+
* When the Keyv instance has an `error` listener, a failed walk returns the bins read before the error.
|
|
367
|
+
*/
|
|
368
|
+
async listBins() {
|
|
369
|
+
if (!this.canIterate()) throw new Error("MockHttpBins: the configured Keyv store does not support iteration; listBins() is unavailable");
|
|
370
|
+
const bins = [];
|
|
371
|
+
for await (const [key, value] of this._keyv.iterator()) if (key.startsWith(BIN_KEY_PREFIX)) bins.push(value.bin);
|
|
372
|
+
return bins;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Delete a bin. Returns true when the bin existed.
|
|
376
|
+
* The delete waits for captures already queued for that bin, so a capture cannot write the bin back.
|
|
377
|
+
*/
|
|
378
|
+
async deleteBin(id) {
|
|
379
|
+
return this.enqueue(id, () => this._keyv.delete(this.binKey(id)));
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Record a request for a bin.
|
|
383
|
+
* Returns `undefined` when the bin does not exist.
|
|
384
|
+
* Throws when Keyv does not store the capture.
|
|
385
|
+
*/
|
|
386
|
+
async recordRequest(binId, raw) {
|
|
387
|
+
const captured = {
|
|
388
|
+
...raw,
|
|
389
|
+
id: this.generateId(),
|
|
390
|
+
binId,
|
|
391
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
392
|
+
};
|
|
393
|
+
return await this.enqueue(binId, () => this.appendRequest(binId, captured)) ? captured : void 0;
|
|
394
|
+
}
|
|
395
|
+
/** List captured requests for a bin, newest first. */
|
|
396
|
+
async getRequests(binId) {
|
|
397
|
+
return [...(await this._keyv.get(this.binKey(binId)))?.requests ?? []];
|
|
398
|
+
}
|
|
399
|
+
/** Look up one captured request. */
|
|
400
|
+
async getRequest(binId, reqId) {
|
|
401
|
+
return (await this._keyv.get(this.binKey(binId)))?.requests.find((request) => request.id === reqId);
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Remove captured requests and keep the bin.
|
|
405
|
+
* Returns false when the bin does not exist.
|
|
406
|
+
* Throws when Keyv does not store the change.
|
|
407
|
+
*/
|
|
408
|
+
async clearRequests(binId) {
|
|
409
|
+
return this.enqueue(binId, async () => {
|
|
410
|
+
const doc = await this._keyv.get(this.binKey(binId));
|
|
411
|
+
if (!doc) return false;
|
|
412
|
+
doc.requests = [];
|
|
413
|
+
doc.bin.requestCount = 0;
|
|
414
|
+
await this.save(doc);
|
|
415
|
+
return true;
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
/** Keyv 6 yields nothing when the store has no iterator. Listing must fail instead. */
|
|
419
|
+
canIterate() {
|
|
420
|
+
return typeof this._keyv.store.iterator === "function";
|
|
421
|
+
}
|
|
422
|
+
/** Returns false when the bin does not exist. */
|
|
423
|
+
async appendRequest(binId, captured) {
|
|
424
|
+
const doc = await this._keyv.get(this.binKey(binId));
|
|
425
|
+
if (!doc) return false;
|
|
426
|
+
doc.requests.unshift(captured);
|
|
427
|
+
while (doc.requests.length > this._maxRequestsPerBin) doc.requests.pop();
|
|
428
|
+
doc.bin.requestCount = doc.requests.length;
|
|
429
|
+
await this.save(doc);
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Write a bin document with a TTL from its lifetime.
|
|
434
|
+
* Keyv returns false or throws when the write fails. Both cases throw here.
|
|
435
|
+
*/
|
|
436
|
+
async save(doc) {
|
|
437
|
+
const message = `MockHttpBins: could not save bin ${doc.bin.id}`;
|
|
438
|
+
let saved;
|
|
439
|
+
try {
|
|
440
|
+
saved = await this._keyv.set(this.binKey(doc.bin.id), doc, this.ttlFor(doc.bin));
|
|
441
|
+
} catch (cause) {
|
|
442
|
+
throw new Error(message, { cause });
|
|
443
|
+
}
|
|
444
|
+
if (!saved) throw new Error(message);
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Run writes for one bin in order.
|
|
448
|
+
* A failed write does not block the next write for that bin.
|
|
449
|
+
*/
|
|
450
|
+
enqueue(id, fn) {
|
|
451
|
+
const run = (this._locks.get(id) ?? Promise.resolve()).then(fn);
|
|
452
|
+
const settled = run.then(() => void 0, () => void 0);
|
|
453
|
+
this._locks.set(id, settled);
|
|
454
|
+
settled.finally(() => {
|
|
455
|
+
if (this._locks.get(id) === settled) this._locks.delete(id);
|
|
456
|
+
});
|
|
457
|
+
return run;
|
|
458
|
+
}
|
|
459
|
+
/** Keyv keeps a key without a TTL forever, so the TTL never drops below 1 ms. */
|
|
460
|
+
ttlFor(bin) {
|
|
461
|
+
return Math.max(1, Date.parse(bin.expiresAt) - Date.now());
|
|
462
|
+
}
|
|
463
|
+
binKey(id) {
|
|
464
|
+
return `${BIN_KEY_PREFIX}${id}`;
|
|
465
|
+
}
|
|
466
|
+
generateId() {
|
|
467
|
+
return randomUUID().replaceAll("-", "").slice(0, this._idLength);
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
//#endregion
|
|
481
471
|
//#region src/routes/anything/index.ts
|
|
482
472
|
const anythingSchema = {
|
|
483
473
|
description: "Will return anything that you send it",
|
|
@@ -907,9 +897,9 @@ const captureSchema = {
|
|
|
907
897
|
const TEXT_CONTENT_TYPE_REGEX = /^(text\/|application\/(json|xml|x-www-form-urlencoded|javascript))/i;
|
|
908
898
|
/**
|
|
909
899
|
* Create a Fastify plugin that registers the catch-all bin capture endpoint
|
|
910
|
-
* at `/b/:id` and `/b/:id/*`, with the provided {@link
|
|
900
|
+
* at `/b/:id` and `/b/:id/*`, with the provided {@link MockHttpBins} injected.
|
|
911
901
|
*/
|
|
912
|
-
const binsCaptureRoute = (
|
|
902
|
+
const binsCaptureRoute = (bins) => {
|
|
913
903
|
return (fastify) => {
|
|
914
904
|
fastify.removeAllContentTypeParsers();
|
|
915
905
|
fastify.addContentTypeParser("*", { parseAs: "buffer" }, (_request, body, done) => {
|
|
@@ -918,10 +908,9 @@ const binsCaptureRoute = (binManager) => {
|
|
|
918
908
|
const handler = async (request, reply) => {
|
|
919
909
|
const { id } = request.params;
|
|
920
910
|
const rest = request.params["*"] ?? "";
|
|
921
|
-
if (!binManager.getBin(id)) return reply.code(404).send({ error: "bin_not_found" });
|
|
922
911
|
const raw = request.body;
|
|
923
912
|
const bodySize = raw?.length ?? 0;
|
|
924
|
-
const max =
|
|
913
|
+
const max = bins.maxBodySize;
|
|
925
914
|
const truncated = bodySize > max;
|
|
926
915
|
const slice = raw && raw.length > 0 ? raw.subarray(0, max) : void 0;
|
|
927
916
|
const contentTypeHeader = request.headers["content-type"];
|
|
@@ -942,7 +931,7 @@ const binsCaptureRoute = (binManager) => {
|
|
|
942
931
|
const prefix = `/b/${id}`;
|
|
943
932
|
const url = request.url.slice(prefix.length) || "/";
|
|
944
933
|
const path = rest ? `/${rest}` : "/";
|
|
945
|
-
const captured =
|
|
934
|
+
const captured = await bins.recordRequest(id, {
|
|
946
935
|
method: request.method,
|
|
947
936
|
url,
|
|
948
937
|
path,
|
|
@@ -955,11 +944,11 @@ const binsCaptureRoute = (binManager) => {
|
|
|
955
944
|
bodyEncoding,
|
|
956
945
|
truncated
|
|
957
946
|
});
|
|
947
|
+
if (!captured) return reply.code(404).send({ error: "bin_not_found" });
|
|
958
948
|
return reply.code(200).send({
|
|
959
949
|
ok: true,
|
|
960
950
|
binId: id,
|
|
961
|
-
|
|
962
|
-
requestId: captured?.id ?? ""
|
|
951
|
+
requestId: captured.id
|
|
963
952
|
});
|
|
964
953
|
};
|
|
965
954
|
const opts = {
|
|
@@ -1174,12 +1163,12 @@ const clearRequestsSchema = {
|
|
|
1174
1163
|
};
|
|
1175
1164
|
/**
|
|
1176
1165
|
* Create a Fastify plugin that registers bin management endpoints, with the
|
|
1177
|
-
* provided {@link
|
|
1166
|
+
* provided {@link MockHttpBins} injected.
|
|
1178
1167
|
*/
|
|
1179
|
-
const binsManagementRoute = (
|
|
1168
|
+
const binsManagementRoute = (bins) => {
|
|
1180
1169
|
return (fastify) => {
|
|
1181
1170
|
fastify.post("/bins", { schema: createBinSchema }, async (request, _reply) => {
|
|
1182
|
-
const bin =
|
|
1171
|
+
const bin = await bins.createBin();
|
|
1183
1172
|
/* v8 ignore next -- @preserve */
|
|
1184
1173
|
const host = request.headers.host ?? request.hostname;
|
|
1185
1174
|
return {
|
|
@@ -1187,29 +1176,28 @@ const binsManagementRoute = (binManager) => {
|
|
|
1187
1176
|
url: `${request.protocol}://${host}/b/${bin.id}`
|
|
1188
1177
|
};
|
|
1189
1178
|
});
|
|
1190
|
-
fastify.get("/bins", { schema: listBinsSchema }, async (_request, _reply) => ({ bins:
|
|
1179
|
+
fastify.get("/bins", { schema: listBinsSchema }, async (_request, _reply) => ({ bins: await bins.listBins() }));
|
|
1191
1180
|
fastify.get("/bins/:id", { schema: getBinSchema }, async (request, reply) => {
|
|
1192
|
-
const bin =
|
|
1181
|
+
const bin = await bins.getBin(request.params.id);
|
|
1193
1182
|
if (!bin) return reply.code(404).send({ error: "bin_not_found" });
|
|
1194
1183
|
return bin;
|
|
1195
1184
|
});
|
|
1196
1185
|
fastify.get("/bins/:id/requests", { schema: listRequestsSchema }, async (request, reply) => {
|
|
1197
|
-
if (!
|
|
1198
|
-
return { requests:
|
|
1186
|
+
if (!await bins.getBin(request.params.id)) return reply.code(404).send({ error: "bin_not_found" });
|
|
1187
|
+
return { requests: await bins.getRequests(request.params.id) };
|
|
1199
1188
|
});
|
|
1200
1189
|
fastify.get("/bins/:id/requests/:reqId", { schema: getRequestSchema }, async (request, reply) => {
|
|
1201
|
-
if (!
|
|
1202
|
-
const captured =
|
|
1190
|
+
if (!await bins.getBin(request.params.id)) return reply.code(404).send({ error: "bin_not_found" });
|
|
1191
|
+
const captured = await bins.getRequest(request.params.id, request.params.reqId);
|
|
1203
1192
|
if (!captured) return reply.code(404).send({ error: "request_not_found" });
|
|
1204
1193
|
return captured;
|
|
1205
1194
|
});
|
|
1206
1195
|
fastify.delete("/bins/:id/requests", { schema: clearRequestsSchema }, async (request, reply) => {
|
|
1207
|
-
if (!
|
|
1208
|
-
binManager.clearRequests(request.params.id);
|
|
1196
|
+
if (!await bins.clearRequests(request.params.id)) return reply.code(404).send({ error: "bin_not_found" });
|
|
1209
1197
|
return reply.code(204).send();
|
|
1210
1198
|
});
|
|
1211
1199
|
fastify.delete("/bins/:id", { schema: deleteBinSchema }, async (request, reply) => {
|
|
1212
|
-
if (!
|
|
1200
|
+
if (!await bins.deleteBin(request.params.id)) return reply.code(404).send({ error: "bin_not_found" });
|
|
1213
1201
|
return reply.code(204).send();
|
|
1214
1202
|
});
|
|
1215
1203
|
};
|
|
@@ -3367,7 +3355,7 @@ A simple HTTP server that can be used to mock HTTP responses for testing purpose
|
|
|
3367
3355
|
|
|
3368
3356
|
[mockhttp.org](https://mockhttp.org) is a free hosted instance of this codebase for testing. It runs on a Cloudflare Worker (no containers): documentation is served from Workers Assets, and mock APIs such as \`/get\` and \`/post\` run in the Worker. The service is globally available and rate-limited (1000 requests per minute per IP) to prevent abuse.
|
|
3369
3357
|
`,
|
|
3370
|
-
version: "
|
|
3358
|
+
version: "3.0.0"
|
|
3371
3359
|
} } };
|
|
3372
3360
|
const withLocalServer = (spec) => {
|
|
3373
3361
|
spec.servers = [{
|
|
@@ -3502,7 +3490,6 @@ var MockHttp = class extends Hookified {
|
|
|
3502
3490
|
_siteDistPath = defaultSiteDistPath;
|
|
3503
3491
|
_logging = true;
|
|
3504
3492
|
_pluginTimeout;
|
|
3505
|
-
_startBins = true;
|
|
3506
3493
|
_httpBin = {
|
|
3507
3494
|
httpMethods: true,
|
|
3508
3495
|
redirects: true,
|
|
@@ -3528,7 +3515,7 @@ var MockHttp = class extends Hookified {
|
|
|
3528
3515
|
_httpsCredentials;
|
|
3529
3516
|
_server;
|
|
3530
3517
|
_taps = new TapManager();
|
|
3531
|
-
_bins = new
|
|
3518
|
+
_bins = new MockHttpBins();
|
|
3532
3519
|
constructor(options) {
|
|
3533
3520
|
super(options?.hookOptions);
|
|
3534
3521
|
if (options?.port) this._port = options.port;
|
|
@@ -3545,7 +3532,6 @@ var MockHttp = class extends Hookified {
|
|
|
3545
3532
|
}
|
|
3546
3533
|
if (options?.logging !== void 0) this._logging = options.logging;
|
|
3547
3534
|
if (options?.pluginTimeout !== void 0) this._pluginTimeout = options.pluginTimeout;
|
|
3548
|
-
if (options?.startBins !== void 0) this._startBins = options.startBins;
|
|
3549
3535
|
if (options?.http2 !== void 0) this._http2 = options.http2;
|
|
3550
3536
|
if (options?.http1 !== void 0) this._http1 = options.http1;
|
|
3551
3537
|
if (options?.https !== void 0) {
|
|
@@ -3679,20 +3665,6 @@ var MockHttp = class extends Hookified {
|
|
|
3679
3665
|
this._pluginTimeout = pluginTimeout;
|
|
3680
3666
|
}
|
|
3681
3667
|
/**
|
|
3682
|
-
* Whether to start the request-bin cleanup timer during `initialize()`.
|
|
3683
|
-
* @default true
|
|
3684
|
-
*/
|
|
3685
|
-
get startBins() {
|
|
3686
|
-
return this._startBins;
|
|
3687
|
-
}
|
|
3688
|
-
/**
|
|
3689
|
-
* Whether to start the request-bin cleanup timer during `initialize()`.
|
|
3690
|
-
* @default true
|
|
3691
|
-
*/
|
|
3692
|
-
set startBins(startBins) {
|
|
3693
|
-
this._startBins = startBins;
|
|
3694
|
-
}
|
|
3695
|
-
/**
|
|
3696
3668
|
* HTTPS configuration. Set to true to auto-generate a self-signed certificate,
|
|
3697
3669
|
* or provide an HttpsOptions object with cert/key or auto-generation options.
|
|
3698
3670
|
*/
|
|
@@ -3798,13 +3770,13 @@ var MockHttp = class extends Hookified {
|
|
|
3798
3770
|
this._taps = taps;
|
|
3799
3771
|
}
|
|
3800
3772
|
/**
|
|
3801
|
-
* The
|
|
3773
|
+
* The MockHttpBins instance for request bins.
|
|
3802
3774
|
*/
|
|
3803
3775
|
get bins() {
|
|
3804
3776
|
return this._bins;
|
|
3805
3777
|
}
|
|
3806
3778
|
/**
|
|
3807
|
-
* The
|
|
3779
|
+
* The MockHttpBins instance for request bins.
|
|
3808
3780
|
*/
|
|
3809
3781
|
set bins(bins) {
|
|
3810
3782
|
this._bins = bins;
|
|
@@ -3874,10 +3846,7 @@ var MockHttp = class extends Hookified {
|
|
|
3874
3846
|
if (auth) await this.registerAuthRoutes();
|
|
3875
3847
|
if (images) await this.registerImageRoutes();
|
|
3876
3848
|
if (dynamicData) await this.registerDynamicDataRoutes();
|
|
3877
|
-
if (bins)
|
|
3878
|
-
await this.registerBinRoutes();
|
|
3879
|
-
if (this._startBins) this._bins.start();
|
|
3880
|
-
}
|
|
3849
|
+
if (bins) await this.registerBinRoutes();
|
|
3881
3850
|
if (this._apiDocs) await this.registerSite();
|
|
3882
3851
|
} catch (error) {
|
|
3883
3852
|
/* v8 ignore next -- @preserve */
|
|
@@ -3908,7 +3877,6 @@ var MockHttp = class extends Hookified {
|
|
|
3908
3877
|
* Close the Fastify server.
|
|
3909
3878
|
*/
|
|
3910
3879
|
async close() {
|
|
3911
|
-
this._bins.stop();
|
|
3912
3880
|
if (this._server) await this._server.close();
|
|
3913
3881
|
}
|
|
3914
3882
|
/**
|
|
@@ -4104,10 +4072,11 @@ const start = async () => {
|
|
|
4104
4072
|
if (process$1.env.LOGGING === "false") mockHttp.logging = false;
|
|
4105
4073
|
/* v8 ignore next -- @preserve */
|
|
4106
4074
|
if (process$1.env.HTTP2 === "true") mockHttp.http2 = true;
|
|
4075
|
+
if (process$1.env.AUTO_DETECT_PORT === "false") mockHttp.autoDetectPort = false;
|
|
4107
4076
|
await mockHttp.start();
|
|
4108
4077
|
return mockHttp;
|
|
4109
4078
|
};
|
|
4110
4079
|
/* v8 ignore next -- @preserve */
|
|
4111
4080
|
if (import.meta.url === `file://${process$1.argv[1]}`) await start();
|
|
4112
4081
|
//#endregion
|
|
4113
|
-
export {
|
|
4082
|
+
export { MockHttpBins, MockHttp as default, MockHttp as mockhttp, generateCertificate, generateCertificateFiles, start };
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jaredwray/mockhttp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Mock Http - Easy way to mock http with httpbin replacement",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.mts",
|
|
8
8
|
"engines": {
|
|
9
|
-
"node": ">=22.
|
|
9
|
+
"node": ">=22.19.0"
|
|
10
10
|
},
|
|
11
11
|
"directories": {
|
|
12
12
|
"test": "test"
|
|
@@ -39,29 +39,32 @@
|
|
|
39
39
|
},
|
|
40
40
|
"homepage": "https://github.com/jaredwray/mockhttp#readme",
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@biomejs/biome": "^2.5.
|
|
43
|
-
"@swc/core": "^1.16.
|
|
42
|
+
"@biomejs/biome": "^2.5.15",
|
|
43
|
+
"@swc/core": "^1.16.18",
|
|
44
44
|
"@types/html-escaper": "^3.0.4",
|
|
45
|
-
"@vitest/coverage-v8": "^5.0.
|
|
46
|
-
"docula": "^3.0.
|
|
45
|
+
"@vitest/coverage-v8": "^5.0.3",
|
|
46
|
+
"docula": "^3.0.1",
|
|
47
47
|
"rimraf": "^6.1.3",
|
|
48
|
-
"tsdown": "^0.23.
|
|
49
|
-
"tsx": "^4.23.
|
|
48
|
+
"tsdown": "^0.23.1",
|
|
49
|
+
"tsx": "^4.23.15",
|
|
50
50
|
"typescript": "7.0.2",
|
|
51
|
-
"
|
|
51
|
+
"vite": "^8.3.4",
|
|
52
|
+
"vitest": "^5.0.3"
|
|
52
53
|
},
|
|
53
54
|
"dependencies": {
|
|
54
|
-
"@fastify/cookie": "^11.1.
|
|
55
|
-
"@fastify/helmet": "^13.1.
|
|
56
|
-
"@fastify/rate-limit": "^11.2.
|
|
57
|
-
"@fastify/static": "^10.1.
|
|
58
|
-
"@fastify/swagger": "^9.
|
|
55
|
+
"@fastify/cookie": "^11.1.3",
|
|
56
|
+
"@fastify/helmet": "^13.1.2",
|
|
57
|
+
"@fastify/rate-limit": "^11.2.1",
|
|
58
|
+
"@fastify/static": "^10.1.6",
|
|
59
|
+
"@fastify/swagger": "^9.9.2",
|
|
60
|
+
"@keyv/mongo": "^6.1.0",
|
|
59
61
|
"detect-port": "^2.1.0",
|
|
60
|
-
"fastify": "^5.12.
|
|
61
|
-
"hookified": "^3.0.
|
|
62
|
+
"fastify": "^5.12.5",
|
|
63
|
+
"hookified": "^3.0.4",
|
|
62
64
|
"html-escaper": "^3.0.3",
|
|
63
|
-
"
|
|
64
|
-
"pino
|
|
65
|
+
"keyv": "^6.1.0",
|
|
66
|
+
"pino": "^10.4.0",
|
|
67
|
+
"pino-pretty": "^13.2.0"
|
|
65
68
|
},
|
|
66
69
|
"files": [
|
|
67
70
|
"dist",
|
package/site/dist/api/index.html
CHANGED
|
@@ -732,7 +732,7 @@
|
|
|
732
732
|
<main class="api-content">
|
|
733
733
|
<section class="api-info">
|
|
734
734
|
<h1 class="api-info__title">Mock HTTP API</h1>
|
|
735
|
-
<span class="api-info__version">
|
|
735
|
+
<span class="api-info__version">v3.0.0</span>
|
|
736
736
|
<div class="api-info__description"><p>A simple HTTP server that can be used to mock HTTP responses for testing purposes. Inspired by <a href="https://httpbin.org/">httpbin</a> and built using <code>nodejs</code> and <code>fastify</code> with the idea of running it via <a href="https://mockhttp.org">https://mockhttp.org</a>, via docker <code>jaredwray/mockhttp</code>, or nodejs <code>npm install @jaredwray/mockhttp</code>.</p>
|
|
737
737
|
<ul>
|
|
738
738
|
<li><a href="https://github.com/jaredwray/mockhttp">GitHub Repository</a></li>
|
|
@@ -15066,7 +15066,7 @@ data = response.json()</code></pre>
|
|
|
15066
15066
|
"remoteAddress": "string",
|
|
15067
15067
|
"contentType": "string",
|
|
15068
15068
|
"bodySize": 0,
|
|
15069
|
-
"body":
|
|
15069
|
+
"body": "string",
|
|
15070
15070
|
"bodyEncoding": "utf8",
|
|
15071
15071
|
"truncated": true,
|
|
15072
15072
|
"capturedAt": "string"
|
|
@@ -15409,7 +15409,7 @@ data = response.json()</code></pre>
|
|
|
15409
15409
|
</div>
|
|
15410
15410
|
<div class="api-schema-prop">
|
|
15411
15411
|
<span class="api-schema-prop-name">body</span>
|
|
15412
|
-
<span class="api-schema-prop-type">
|
|
15412
|
+
<span class="api-schema-prop-type">string</span>
|
|
15413
15413
|
<span class="api-schema-prop-desc"></span>
|
|
15414
15414
|
</div>
|
|
15415
15415
|
<div class="api-schema-prop">
|
|
@@ -15445,7 +15445,7 @@ data = response.json()</code></pre>
|
|
|
15445
15445
|
"remoteAddress": "string",
|
|
15446
15446
|
"contentType": "string",
|
|
15447
15447
|
"bodySize": 0,
|
|
15448
|
-
"body":
|
|
15448
|
+
"body": "string",
|
|
15449
15449
|
"bodyEncoding": "utf8",
|
|
15450
15450
|
"truncated": true,
|
|
15451
15451
|
"capturedAt": "string"
|
|
@@ -17716,7 +17716,7 @@ data = response.json()</code></pre>
|
|
|
17716
17716
|
<a href="https://docula.org/" target="_blank" rel="noopener noreferrer" class="footer__link" aria-label="Docula">
|
|
17717
17717
|
<svg class="footer__bat" xmlns="http://www.w3.org/2000/svg" viewBox="70 110 360 205" fill="currentColor"><path d="M420,173c-16.76-18-72.78-36.65-131.59-30.58A160.15,160.15,0,0,0,269,120.55c-3.78,4.52-11.15,9.45-19,9.45s-15.23-4.93-19-9.45a160.15,160.15,0,0,0-19.4,21.83C152.78,136.31,96.76,154.94,80,173c48.86,5.86,79.5,34.09,79.5,72.06,52.37,0,77,28,90.5,59.94C263.54,273,288.13,245,340.5,245,340.5,207,371.14,178.8,420,173Z"/><circle cx="239.19" cy="145.36" r="3.32" fill="#fff"/><circle cx="260.81" cy="145.36" r="3.32" fill="#fff"/></svg>
|
|
17718
17718
|
</a>
|
|
17719
|
-
<span class="footer__updated">updated: 2026-
|
|
17719
|
+
<span class="footer__updated">updated: 2026-10-20</span>
|
|
17720
17720
|
</footer>
|
|
17721
17721
|
<script src="/css/highlight/highlight.min.js"></script>
|
|
17722
17722
|
<script>
|