@jaredwray/mockhttp 1.6.1 → 1.8.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 +422 -0
- package/dist/index.d.mts +157 -1
- package/dist/index.mjs +574 -8
- package/package.json +87 -89
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ A simple HTTP server that can be used to mock HTTP responses for testing purpose
|
|
|
13
13
|
# Features
|
|
14
14
|
* All the features of [httpbin](https://httpbin.org/)
|
|
15
15
|
* Taps - Inject custom responses for testing and development
|
|
16
|
+
* Bins - Capture and inspect incoming HTTP requests (great for webhook debugging)
|
|
16
17
|
* `@fastify/helmet` built in by default
|
|
17
18
|
* Built with `nodejs`, `typescript`, and `fastify`
|
|
18
19
|
* Deploy via `docker` or `nodejs`
|
|
@@ -28,6 +29,7 @@ A simple HTTP server that can be used to mock HTTP responses for testing purpose
|
|
|
28
29
|
- [HTTPS Support](#https-support)
|
|
29
30
|
- [HTTP/2 Support](#http2-support)
|
|
30
31
|
- [Response Injection (Tap Feature)](#response-injection-tap-feature)
|
|
32
|
+
- [Request Bins](#request-bins)
|
|
31
33
|
- [Rate Limiting](#rate-limiting)
|
|
32
34
|
- [Logging](#logging)
|
|
33
35
|
- [Flexible URL Matching](#flexible-url-matching)
|
|
@@ -431,6 +433,362 @@ const tap = mock.taps.inject(
|
|
|
431
433
|
);
|
|
432
434
|
```
|
|
433
435
|
|
|
436
|
+
# Request Bins
|
|
437
|
+
|
|
438
|
+
Bins are ephemeral URL endpoints that **capture incoming HTTP requests** so you
|
|
439
|
+
can inspect them later. They're the inverse of Taps: instead of injecting a
|
|
440
|
+
response, they record everything they receive. Useful for:
|
|
441
|
+
|
|
442
|
+
- **Debugging webhooks** — point Stripe / GitHub / Slack at a bin URL and see
|
|
443
|
+
exactly what they send
|
|
444
|
+
- **Exercising client SDKs** — verify your SDK sends the request shape you expect
|
|
445
|
+
- **Recording fixtures** — capture real traffic to replay in tests later
|
|
446
|
+
|
|
447
|
+
There are two URL prefixes:
|
|
448
|
+
|
|
449
|
+
- **`/bins`** — JSON management API: create, list, inspect, delete bins
|
|
450
|
+
- **`/b/:id`** — the capture URL. Any HTTP method, any sub-path, any body sent
|
|
451
|
+
here is stored against bin `:id`
|
|
452
|
+
|
|
453
|
+
## Quick Start
|
|
454
|
+
|
|
455
|
+
```bash
|
|
456
|
+
# 1. create a bin
|
|
457
|
+
curl -s -X POST http://localhost:3000/bins
|
|
458
|
+
# → { "id": "abc123def456", "url": "http://localhost:3000/b/abc123def456",
|
|
459
|
+
# "createdAt": "2026-05-18T12:00:00.000Z",
|
|
460
|
+
# "expiresAt": "2026-05-19T12:00:00.000Z",
|
|
461
|
+
# "requestCount": 0 }
|
|
462
|
+
|
|
463
|
+
# 2. send anything to the capture URL
|
|
464
|
+
curl -X POST "http://localhost:3000/b/abc123def456/webhook?source=stripe" \
|
|
465
|
+
-H 'content-type: application/json' \
|
|
466
|
+
-d '{"event":"payment.succeeded"}'
|
|
467
|
+
# → { "ok": true, "binId": "abc123def456", "requestId": "f9c2e1a40b3d" }
|
|
468
|
+
|
|
469
|
+
# 3. list captured requests (newest first)
|
|
470
|
+
curl http://localhost:3000/bins/abc123def456/requests
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
## Endpoints
|
|
474
|
+
|
|
475
|
+
| Method | Path | Description |
|
|
476
|
+
|--------|------|-------------|
|
|
477
|
+
| POST | `/bins` | Create a new bin |
|
|
478
|
+
| GET | `/bins` | List all active bins |
|
|
479
|
+
| GET | `/bins/:id` | Get bin metadata |
|
|
480
|
+
| GET | `/bins/:id/requests` | List captured requests (newest first) |
|
|
481
|
+
| GET | `/bins/:id/requests/:reqId` | Get a single captured request |
|
|
482
|
+
| DELETE | `/bins/:id/requests` | Clear all captured requests in a bin |
|
|
483
|
+
| DELETE | `/bins/:id` | Delete a bin |
|
|
484
|
+
| ANY | `/b/:id` and `/b/:id/*` | Capture requests sent to a bin |
|
|
485
|
+
|
|
486
|
+
## End-to-End Webhook Debugging Example
|
|
487
|
+
|
|
488
|
+
A complete walkthrough using curl:
|
|
489
|
+
|
|
490
|
+
```bash
|
|
491
|
+
BASE=http://localhost:3000
|
|
492
|
+
|
|
493
|
+
# Create the bin and grab its id
|
|
494
|
+
ID=$(curl -s -X POST $BASE/bins | jq -r .id)
|
|
495
|
+
echo "Bin URL: $BASE/b/$ID"
|
|
496
|
+
|
|
497
|
+
# Simulate a Stripe webhook
|
|
498
|
+
curl -s -X POST "$BASE/b/$ID/stripe/events?secret=whsec_test" \
|
|
499
|
+
-H 'content-type: application/json' \
|
|
500
|
+
-H 'stripe-signature: t=1700000000,v1=abc123' \
|
|
501
|
+
-d '{"id":"evt_1","type":"payment_intent.succeeded","data":{"object":{"amount":2000}}}'
|
|
502
|
+
|
|
503
|
+
# Simulate a GitHub webhook
|
|
504
|
+
curl -s -X POST "$BASE/b/$ID/github" \
|
|
505
|
+
-H 'content-type: application/json' \
|
|
506
|
+
-H 'x-github-event: pull_request' \
|
|
507
|
+
-d '{"action":"opened","number":42}'
|
|
508
|
+
|
|
509
|
+
# Inspect everything that arrived
|
|
510
|
+
curl -s "$BASE/bins/$ID/requests" | jq
|
|
511
|
+
|
|
512
|
+
# Drill into the most recent one
|
|
513
|
+
RID=$(curl -s "$BASE/bins/$ID/requests" | jq -r '.requests[0].id')
|
|
514
|
+
curl -s "$BASE/bins/$ID/requests/$RID" | jq
|
|
515
|
+
# {
|
|
516
|
+
# "id": "f9c2e1a40b3d",
|
|
517
|
+
# "binId": "abc123def456",
|
|
518
|
+
# "method": "POST",
|
|
519
|
+
# "url": "/github",
|
|
520
|
+
# "path": "/github",
|
|
521
|
+
# "query": {},
|
|
522
|
+
# "headers": {
|
|
523
|
+
# "host": "localhost:3000",
|
|
524
|
+
# "content-type": "application/json",
|
|
525
|
+
# "x-github-event": "pull_request"
|
|
526
|
+
# },
|
|
527
|
+
# "remoteAddress": "127.0.0.1",
|
|
528
|
+
# "contentType": "application/json",
|
|
529
|
+
# "bodySize": 32,
|
|
530
|
+
# "body": "{\"action\":\"opened\",\"number\":42}",
|
|
531
|
+
# "bodyEncoding": "utf8",
|
|
532
|
+
# "truncated": false,
|
|
533
|
+
# "capturedAt": "2026-05-18T12:00:01.234Z"
|
|
534
|
+
# }
|
|
535
|
+
|
|
536
|
+
# Clean up
|
|
537
|
+
curl -s -X DELETE "$BASE/bins/$ID/requests" # clear captures, keep the bin
|
|
538
|
+
curl -s -X DELETE "$BASE/bins/$ID" # delete the bin entirely
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
## Captured Request Shape
|
|
542
|
+
|
|
543
|
+
```json
|
|
544
|
+
{
|
|
545
|
+
"id": "f9c2e1a40b3d",
|
|
546
|
+
"binId": "abc123def456",
|
|
547
|
+
"method": "POST",
|
|
548
|
+
"url": "/webhook?source=stripe",
|
|
549
|
+
"path": "/webhook",
|
|
550
|
+
"query": { "source": "stripe" },
|
|
551
|
+
"headers": { "content-type": "application/json", "...": "..." },
|
|
552
|
+
"remoteAddress": "127.0.0.1",
|
|
553
|
+
"contentType": "application/json",
|
|
554
|
+
"bodySize": 32,
|
|
555
|
+
"body": "{\"event\":\"payment.succeeded\"}",
|
|
556
|
+
"bodyEncoding": "utf8",
|
|
557
|
+
"truncated": false,
|
|
558
|
+
"capturedAt": "2026-05-18T12:00:00.000Z"
|
|
559
|
+
}
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
- **`url`** is the request URL relative to the bin (everything after `/b/:id`).
|
|
563
|
+
- **`path`** is the sub-path only, without the query string.
|
|
564
|
+
- **`bodyEncoding`** is `"utf8"` for text content types (text/*, application/json,
|
|
565
|
+
application/xml, application/x-www-form-urlencoded, application/javascript),
|
|
566
|
+
`"base64"` for everything else (binary payloads), or `"none"` when no body
|
|
567
|
+
was sent.
|
|
568
|
+
- **`truncated`** is `true` when the body exceeded `maxBodySize` and was cut
|
|
569
|
+
off. `bodySize` reflects the original length.
|
|
570
|
+
|
|
571
|
+
## Body Encoding Examples
|
|
572
|
+
|
|
573
|
+
### Text Content (UTF-8)
|
|
574
|
+
|
|
575
|
+
JSON, XML, form-encoded, plain text, and JavaScript are stored as UTF-8 strings
|
|
576
|
+
so you can read them directly:
|
|
577
|
+
|
|
578
|
+
```bash
|
|
579
|
+
curl -X POST "http://localhost:3000/b/$ID" \
|
|
580
|
+
-H 'content-type: application/json' \
|
|
581
|
+
-d '{"hello":"world"}'
|
|
582
|
+
|
|
583
|
+
curl -s "http://localhost:3000/bins/$ID/requests" | jq '.requests[0] | {bodyEncoding, body}'
|
|
584
|
+
# {
|
|
585
|
+
# "bodyEncoding": "utf8",
|
|
586
|
+
# "body": "{\"hello\":\"world\"}"
|
|
587
|
+
# }
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
### Binary Content (Base64)
|
|
591
|
+
|
|
592
|
+
Any non-text content type is base64-encoded, preserving the bytes exactly:
|
|
593
|
+
|
|
594
|
+
```bash
|
|
595
|
+
# Upload a PNG to the bin
|
|
596
|
+
curl -X POST "http://localhost:3000/b/$ID/upload" \
|
|
597
|
+
-H 'content-type: image/png' \
|
|
598
|
+
--data-binary @logo.png
|
|
599
|
+
|
|
600
|
+
curl -s "http://localhost:3000/bins/$ID/requests" | jq '.requests[0] | {bodyEncoding, bodySize}'
|
|
601
|
+
# {
|
|
602
|
+
# "bodyEncoding": "base64",
|
|
603
|
+
# "bodySize": 14823
|
|
604
|
+
# }
|
|
605
|
+
|
|
606
|
+
# Decode the body back to bytes
|
|
607
|
+
curl -s "http://localhost:3000/bins/$ID/requests" \
|
|
608
|
+
| jq -r '.requests[0].body' | base64 -d > recovered.png
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
### Truncation
|
|
612
|
+
|
|
613
|
+
Bodies larger than `maxBodySize` (1 MiB by default) are cut off and flagged:
|
|
614
|
+
|
|
615
|
+
```javascript
|
|
616
|
+
// In test setup
|
|
617
|
+
import { MockHttp, BinManager } from '@jaredwray/mockhttp';
|
|
618
|
+
const mock = new MockHttp();
|
|
619
|
+
mock.bins = new BinManager({ maxBodySize: 1024 }); // 1 KiB cap
|
|
620
|
+
await mock.start();
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
```bash
|
|
624
|
+
# Send 5000 bytes to a bin with a 1 KiB cap
|
|
625
|
+
head -c 5000 /dev/urandom | curl -X POST "http://localhost:3000/b/$ID" \
|
|
626
|
+
-H 'content-type: application/octet-stream' \
|
|
627
|
+
--data-binary @-
|
|
628
|
+
|
|
629
|
+
curl -s "http://localhost:3000/bins/$ID/requests" | jq '.requests[0] | {bodySize, truncated, body_len: (.body | length)}'
|
|
630
|
+
# {
|
|
631
|
+
# "bodySize": 5000, ← original size
|
|
632
|
+
# "truncated": true,
|
|
633
|
+
# "body_len": 1368 ← base64 of the first 1024 bytes
|
|
634
|
+
# }
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
## FIFO Eviction Example
|
|
638
|
+
|
|
639
|
+
Once a bin reaches `maxRequestsPerBin` captures (100 by default), the oldest
|
|
640
|
+
ones are dropped:
|
|
641
|
+
|
|
642
|
+
```bash
|
|
643
|
+
# Send 105 requests
|
|
644
|
+
for i in $(seq 1 105); do
|
|
645
|
+
curl -s -X POST "http://localhost:3000/b/$ID/event/$i" > /dev/null
|
|
646
|
+
done
|
|
647
|
+
|
|
648
|
+
# The bin holds the most recent 100; the first 5 are gone
|
|
649
|
+
curl -s "http://localhost:3000/bins/$ID/requests" | jq '.requests | length'
|
|
650
|
+
# 100
|
|
651
|
+
|
|
652
|
+
# Newest is at the top
|
|
653
|
+
curl -s "http://localhost:3000/bins/$ID/requests" | jq '.requests[0].path'
|
|
654
|
+
# "/event/105"
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
## Programmatic Access
|
|
658
|
+
|
|
659
|
+
The bin manager is exposed on the `MockHttp` instance as `mock.bins`. This
|
|
660
|
+
makes it easy to drive bins from a test suite without going through HTTP:
|
|
661
|
+
|
|
662
|
+
```javascript
|
|
663
|
+
import { MockHttp } from '@jaredwray/mockhttp';
|
|
664
|
+
|
|
665
|
+
const mock = new MockHttp({ logging: false });
|
|
666
|
+
await mock.start();
|
|
667
|
+
|
|
668
|
+
// Create a bin
|
|
669
|
+
const bin = mock.bins.createBin();
|
|
670
|
+
console.log(`Webhook target: http://localhost:${mock.port}/b/${bin.id}`);
|
|
671
|
+
|
|
672
|
+
// ... point your code-under-test at that URL ...
|
|
673
|
+
|
|
674
|
+
// Read captured requests (newest first)
|
|
675
|
+
const requests = mock.bins.getRequests(bin.id);
|
|
676
|
+
console.log(`Captured ${requests.length} requests`);
|
|
677
|
+
|
|
678
|
+
for (const req of requests) {
|
|
679
|
+
console.log(`${req.method} ${req.path} (${req.bodySize} bytes)`);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Clean up
|
|
683
|
+
mock.bins.deleteBin(bin.id);
|
|
684
|
+
await mock.close(); // also stops the bin cleanup timer
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
### Using Bins in a Vitest Test
|
|
688
|
+
|
|
689
|
+
```typescript
|
|
690
|
+
import { afterAll, beforeAll, expect, test } from 'vitest';
|
|
691
|
+
import { MockHttp } from '@jaredwray/mockhttp';
|
|
692
|
+
|
|
693
|
+
let mock: MockHttp;
|
|
694
|
+
|
|
695
|
+
beforeAll(async () => {
|
|
696
|
+
mock = new MockHttp({ logging: false });
|
|
697
|
+
await mock.start();
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
afterAll(async () => {
|
|
701
|
+
await mock.close();
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
test('my SDK sends the right webhook payload', async () => {
|
|
705
|
+
const bin = mock.bins.createBin();
|
|
706
|
+
const webhookUrl = `http://localhost:${mock.port}/b/${bin.id}`;
|
|
707
|
+
|
|
708
|
+
// Drive your SDK at the bin
|
|
709
|
+
await mySdk.notify(webhookUrl, { event: 'user.created', id: 42 });
|
|
710
|
+
|
|
711
|
+
// Assert on what actually arrived
|
|
712
|
+
const [captured] = mock.bins.getRequests(bin.id);
|
|
713
|
+
expect(captured.method).toBe('POST');
|
|
714
|
+
expect(captured.contentType).toBe('application/json');
|
|
715
|
+
expect(JSON.parse(captured.body!)).toEqual({
|
|
716
|
+
event: 'user.created',
|
|
717
|
+
id: 42,
|
|
718
|
+
});
|
|
719
|
+
expect(captured.headers['x-signature']).toBeDefined();
|
|
720
|
+
});
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
## Configuration
|
|
724
|
+
|
|
725
|
+
Bins are enabled by default. To disable the routes entirely:
|
|
726
|
+
|
|
727
|
+
```javascript
|
|
728
|
+
const mock = new MockHttp({
|
|
729
|
+
httpBin: { bins: false },
|
|
730
|
+
});
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
To tune limits, replace the default `BinManager` before starting:
|
|
734
|
+
|
|
735
|
+
```javascript
|
|
736
|
+
import { MockHttp, BinManager } from '@jaredwray/mockhttp';
|
|
737
|
+
|
|
738
|
+
const mock = new MockHttp();
|
|
739
|
+
mock.bins = new BinManager({
|
|
740
|
+
defaultTtlMs: 60 * 60 * 1000, // 1 hour (default: 24h)
|
|
741
|
+
maxRequestsPerBin: 500, // (default: 100)
|
|
742
|
+
maxBodySize: 5 * 1024 * 1024, // 5 MiB (default: 1 MiB)
|
|
743
|
+
idLength: 16, // (default: 12)
|
|
744
|
+
cleanupIntervalMs: 30 * 1000, // sweep every 30s (default: 60s)
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
await mock.start();
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
### Defaults
|
|
751
|
+
|
|
752
|
+
| Option | Default | Description |
|
|
753
|
+
|--------|---------|-------------|
|
|
754
|
+
| `defaultTtlMs` | `86400000` (24h) | Bin lifetime. Expired bins return 404 and are lazily removed. |
|
|
755
|
+
| `maxRequestsPerBin` | `100` | When exceeded, oldest captures are dropped (FIFO). |
|
|
756
|
+
| `maxBodySize` | `1048576` (1 MiB) | Larger bodies are truncated; `truncated: true` is set on the capture. |
|
|
757
|
+
| `idLength` | `12` | Length of generated bin and request ids. |
|
|
758
|
+
| `cleanupIntervalMs` | `60000` (1 min) | How often expired bins are swept. The timer is `unref()`'d so it never keeps the process alive. |
|
|
759
|
+
|
|
760
|
+
### Pluggable Storage
|
|
761
|
+
|
|
762
|
+
`BinManager` accepts a `store: BinStore` so you can plug in alternative
|
|
763
|
+
backends (Redis, SQLite, etc.) without changing the rest of the codebase. The
|
|
764
|
+
default `InMemoryBinStore` keeps state in process memory.
|
|
765
|
+
|
|
766
|
+
```typescript
|
|
767
|
+
import {
|
|
768
|
+
BinManager,
|
|
769
|
+
InMemoryBinStore,
|
|
770
|
+
type Bin,
|
|
771
|
+
type BinStore,
|
|
772
|
+
type CapturedRequest,
|
|
773
|
+
} from '@jaredwray/mockhttp';
|
|
774
|
+
|
|
775
|
+
class RedisBinStore implements BinStore {
|
|
776
|
+
createBin(bin: Bin): void { /* SET bin:${bin.id} ... */ }
|
|
777
|
+
getBin(id: string): Bin | undefined { /* GET ... */ }
|
|
778
|
+
listBins(): Bin[] { /* SCAN ... */ }
|
|
779
|
+
deleteBin(id: string): boolean { /* DEL ... */ }
|
|
780
|
+
addRequest(binId: string, req: CapturedRequest, max: number): void {
|
|
781
|
+
/* LPUSH bin:${binId}:requests + LTRIM to max */
|
|
782
|
+
}
|
|
783
|
+
getRequests(binId: string): CapturedRequest[] { /* LRANGE ... */ }
|
|
784
|
+
getRequest(binId: string, reqId: string): CapturedRequest | undefined { /* ... */ }
|
|
785
|
+
clearRequests(binId: string): void { /* DEL bin:${binId}:requests */ }
|
|
786
|
+
cleanupExpired(now: number): string[] { /* scan + delete */ return []; }
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
mock.bins = new BinManager({ store: new RedisBinStore() });
|
|
790
|
+
```
|
|
791
|
+
|
|
434
792
|
# Rate Limiting
|
|
435
793
|
|
|
436
794
|
MockHttp supports rate limiting using [@fastify/rate-limit](https://github.com/fastify/fastify-rate-limit). Rate limiting is **enabled by default** at **1000 requests per minute** with **localhost (127.0.0.1 and ::1) excluded** from rate limiting.
|
|
@@ -654,6 +1012,7 @@ new MockHttp(options?)
|
|
|
654
1012
|
- `anything?`: boolean - Enable anything routes (default: true)
|
|
655
1013
|
- `auth?`: boolean - Enable authentication routes (default: true)
|
|
656
1014
|
- `images?`: boolean - Enable image routes (default: true)
|
|
1015
|
+
- `bins?`: boolean - Enable request bin routes /bins and /b/:id (default: true)
|
|
657
1016
|
- `https?`: boolean | HttpsOptions - Enable HTTPS with auto-generated or custom certificates (default: undefined/disabled)
|
|
658
1017
|
- `http2?`: boolean - Enable HTTP/2 support (default: false)
|
|
659
1018
|
- `http1?`: boolean - Allow HTTP/1.1 fallback when using HTTP/2 with HTTPS (default: true)
|
|
@@ -675,6 +1034,7 @@ new MockHttp(options?)
|
|
|
675
1034
|
- `http1`: boolean - Get/set HTTP/1.1 fallback for HTTP/2 with HTTPS
|
|
676
1035
|
- `server`: FastifyInstance - Get/set the Fastify server instance
|
|
677
1036
|
- `taps`: TapManager - Get/set the TapManager instance
|
|
1037
|
+
- `bins`: BinManager - Get/set the BinManager instance for request bins
|
|
678
1038
|
|
|
679
1039
|
### Methods
|
|
680
1040
|
|
|
@@ -736,6 +1096,68 @@ Register authentication routes (basic, bearer, digest, hidden-basic).
|
|
|
736
1096
|
|
|
737
1097
|
Register image routes (jpeg, png, svg, webp) with content negotiation support.
|
|
738
1098
|
|
|
1099
|
+
#### `async registerBinRoutes(fastifyInstance?)`
|
|
1100
|
+
|
|
1101
|
+
Register the request bin routes — management at `/bins` and capture at `/b/:id`.
|
|
1102
|
+
|
|
1103
|
+
## Bins (Request Capture)
|
|
1104
|
+
|
|
1105
|
+
Access the BinManager via `mockHttp.bins` to manage request bins programmatically. See [Request Bins](#request-bins) for usage examples.
|
|
1106
|
+
|
|
1107
|
+
### `bins.createBin()`
|
|
1108
|
+
|
|
1109
|
+
Create a new bin with the configured TTL.
|
|
1110
|
+
|
|
1111
|
+
**Returns:** `Bin` — `{ id, createdAt, expiresAt, requestCount }`
|
|
1112
|
+
|
|
1113
|
+
### `bins.getBin(id)`
|
|
1114
|
+
|
|
1115
|
+
Look up a bin by id. Returns `undefined` if the bin does not exist or has expired (and lazily removes the expired entry).
|
|
1116
|
+
|
|
1117
|
+
**Returns:** `Bin | undefined`
|
|
1118
|
+
|
|
1119
|
+
### `bins.listBins()`
|
|
1120
|
+
|
|
1121
|
+
List all non-expired bins.
|
|
1122
|
+
|
|
1123
|
+
**Returns:** `Bin[]`
|
|
1124
|
+
|
|
1125
|
+
### `bins.deleteBin(id)`
|
|
1126
|
+
|
|
1127
|
+
Delete a bin and all its captured requests.
|
|
1128
|
+
|
|
1129
|
+
**Returns:** `boolean` — `true` if the bin existed
|
|
1130
|
+
|
|
1131
|
+
### `bins.getRequests(binId)`
|
|
1132
|
+
|
|
1133
|
+
List captured requests for a bin, newest first.
|
|
1134
|
+
|
|
1135
|
+
**Returns:** `CapturedRequest[]`
|
|
1136
|
+
|
|
1137
|
+
### `bins.getRequest(binId, reqId)`
|
|
1138
|
+
|
|
1139
|
+
Look up a single captured request.
|
|
1140
|
+
|
|
1141
|
+
**Returns:** `CapturedRequest | undefined`
|
|
1142
|
+
|
|
1143
|
+
### `bins.clearRequests(binId)`
|
|
1144
|
+
|
|
1145
|
+
Clear all captured requests in a bin (the bin itself is preserved).
|
|
1146
|
+
|
|
1147
|
+
### `bins.recordRequest(binId, raw)`
|
|
1148
|
+
|
|
1149
|
+
Manually record a captured request. Returns `undefined` if the bin does not exist or is expired. Normally invoked by the capture route, but available for programmatic use.
|
|
1150
|
+
|
|
1151
|
+
**Returns:** `CapturedRequest | undefined`
|
|
1152
|
+
|
|
1153
|
+
### `bins.start()` / `bins.stop()`
|
|
1154
|
+
|
|
1155
|
+
Start or stop the periodic cleanup of expired bins. `MockHttp.start()` and `MockHttp.close()` call these automatically. The interval is `unref()`'d so it never keeps the Node process alive.
|
|
1156
|
+
|
|
1157
|
+
### `bins.maxBodySize` / `bins.maxRequestsPerBin` / `bins.defaultTtlMs`
|
|
1158
|
+
|
|
1159
|
+
Read-only getters for the currently-configured limits.
|
|
1160
|
+
|
|
739
1161
|
## Taps (Response Injection)
|
|
740
1162
|
|
|
741
1163
|
Access the TapManager via `mockHttp.taps` to inject custom responses.
|
package/dist/index.d.mts
CHANGED
|
@@ -2,6 +2,147 @@ import { RateLimitPluginOptions } from "@fastify/rate-limit";
|
|
|
2
2
|
import { FastifyInstance, FastifyRequest } from "fastify";
|
|
3
3
|
import { Hookified, HookifiedOptions } from "hookified";
|
|
4
4
|
|
|
5
|
+
//#region src/bin-store.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* A request captured by a bin, including method, headers, body and metadata.
|
|
8
|
+
*/
|
|
9
|
+
type CapturedRequest = {
|
|
10
|
+
/** Unique identifier for this captured request */id: string; /** Identifier of the bin that captured this request */
|
|
11
|
+
binId: string; /** HTTP method (GET, POST, etc.) */
|
|
12
|
+
method: string; /** Request URL path + query, relative to the bin (e.g. "/foo?x=1") */
|
|
13
|
+
url: string; /** Sub-path after the bin id ("/" if the request hit the bin root) */
|
|
14
|
+
path: string; /** Parsed query string parameters */
|
|
15
|
+
query: Record<string, string | string[]>; /** Request headers */
|
|
16
|
+
headers: Record<string, string | string[]>; /** Client IP address as reported by Fastify */
|
|
17
|
+
remoteAddress: string; /** Content-Type header value, if any */
|
|
18
|
+
contentType?: string; /** Original body length in bytes */
|
|
19
|
+
bodySize: number; /** Body payload as utf8 text, base64-encoded bytes, or null when empty */
|
|
20
|
+
body: string | null; /** Encoding used for the `body` field */
|
|
21
|
+
bodyEncoding: "utf8" | "base64" | "none"; /** True if the body was longer than the configured `maxBodySize` */
|
|
22
|
+
truncated: boolean; /** ISO timestamp of when the request was captured */
|
|
23
|
+
capturedAt: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Metadata for a bin (without its captured requests).
|
|
27
|
+
*/
|
|
28
|
+
type Bin = {
|
|
29
|
+
/** Unique short identifier for the bin */id: string; /** ISO timestamp of bin creation */
|
|
30
|
+
createdAt: string; /** ISO timestamp at which the bin expires */
|
|
31
|
+
expiresAt: string; /** Number of requests currently stored in the bin */
|
|
32
|
+
requestCount: number;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Storage backend for bins and their captured requests. The default
|
|
36
|
+
* implementation is in-memory; alternative backends (Redis, SQLite, etc.) can
|
|
37
|
+
* implement this interface without changes to the rest of the codebase.
|
|
38
|
+
*/
|
|
39
|
+
interface BinStore {
|
|
40
|
+
/** Create a new bin record. */
|
|
41
|
+
createBin(bin: Bin): void;
|
|
42
|
+
/** Look up a bin by id. */
|
|
43
|
+
getBin(id: string): Bin | undefined;
|
|
44
|
+
/** Return all bins. */
|
|
45
|
+
listBins(): Bin[];
|
|
46
|
+
/** Remove a bin and all its captured requests. Returns true if removed. */
|
|
47
|
+
deleteBin(id: string): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Append a captured request to a bin, dropping the oldest if the bin has
|
|
50
|
+
* already reached `max` requests (FIFO).
|
|
51
|
+
*/
|
|
52
|
+
addRequest(binId: string, req: CapturedRequest, max: number): void;
|
|
53
|
+
/** List captured requests for a bin (newest first). */
|
|
54
|
+
getRequests(binId: string): CapturedRequest[];
|
|
55
|
+
/** Look up a single captured request by id. */
|
|
56
|
+
getRequest(binId: string, reqId: string): CapturedRequest | undefined;
|
|
57
|
+
/** Clear all captured requests in a bin (the bin itself is preserved). */
|
|
58
|
+
clearRequests(binId: string): void;
|
|
59
|
+
/**
|
|
60
|
+
* Remove bins whose `expiresAt` is in the past relative to `now`.
|
|
61
|
+
* Returns the ids of bins that were removed.
|
|
62
|
+
*/
|
|
63
|
+
cleanupExpired(now: number): string[];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* In-memory implementation of {@link BinStore} backed by two Maps. State is
|
|
67
|
+
* lost when the process exits.
|
|
68
|
+
*/
|
|
69
|
+
declare class InMemoryBinStore implements BinStore {
|
|
70
|
+
private readonly _bins;
|
|
71
|
+
private readonly _requests;
|
|
72
|
+
createBin(bin: Bin): void;
|
|
73
|
+
getBin(id: string): Bin | undefined;
|
|
74
|
+
listBins(): Bin[];
|
|
75
|
+
deleteBin(id: string): boolean;
|
|
76
|
+
addRequest(binId: string, req: CapturedRequest, max: number): void;
|
|
77
|
+
getRequests(binId: string): CapturedRequest[];
|
|
78
|
+
getRequest(binId: string, reqId: string): CapturedRequest | undefined;
|
|
79
|
+
clearRequests(binId: string): void;
|
|
80
|
+
cleanupExpired(now: number): string[];
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/bin-manager.d.ts
|
|
84
|
+
/**
|
|
85
|
+
* Configuration options for {@link BinManager}.
|
|
86
|
+
*/
|
|
87
|
+
type BinManagerOptions = {
|
|
88
|
+
/** Storage backend. Defaults to a new {@link InMemoryBinStore}. */store?: BinStore; /** Bin lifetime in milliseconds. Defaults to 24 hours. */
|
|
89
|
+
defaultTtlMs?: number; /** Maximum number of requests retained per bin (FIFO). Defaults to 100. */
|
|
90
|
+
maxRequestsPerBin?: number; /** Maximum captured body size in bytes. Larger bodies are truncated. Defaults to 1 MiB. */
|
|
91
|
+
maxBodySize?: number; /** Length of generated bin and request ids. Defaults to 12. */
|
|
92
|
+
idLength?: number; /** Interval between cleanup sweeps in milliseconds. Defaults to 1 minute. */
|
|
93
|
+
cleanupIntervalMs?: number;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Manages request bins: ephemeral URL endpoints that capture incoming HTTP
|
|
97
|
+
* requests so they can be inspected later.
|
|
98
|
+
*/
|
|
99
|
+
declare class BinManager {
|
|
100
|
+
private readonly _store;
|
|
101
|
+
private readonly _defaultTtlMs;
|
|
102
|
+
private readonly _maxRequestsPerBin;
|
|
103
|
+
private readonly _maxBodySize;
|
|
104
|
+
private readonly _idLength;
|
|
105
|
+
private readonly _cleanupIntervalMs;
|
|
106
|
+
private _cleanupTimer;
|
|
107
|
+
constructor(options?: BinManagerOptions);
|
|
108
|
+
/** Underlying storage backend. */
|
|
109
|
+
get store(): BinStore;
|
|
110
|
+
/** Maximum captured body size in bytes. */
|
|
111
|
+
get maxBodySize(): number;
|
|
112
|
+
/** Maximum number of requests retained per bin. */
|
|
113
|
+
get maxRequestsPerBin(): number;
|
|
114
|
+
/** Default bin lifetime in milliseconds. */
|
|
115
|
+
get defaultTtlMs(): number;
|
|
116
|
+
/** Create a new bin with the configured TTL. */
|
|
117
|
+
createBin(): Bin;
|
|
118
|
+
/**
|
|
119
|
+
* Look up a bin by id. Returns `undefined` if the bin does not exist or
|
|
120
|
+
* has expired (in which case it is also lazily removed).
|
|
121
|
+
*/
|
|
122
|
+
getBin(id: string): Bin | undefined;
|
|
123
|
+
/** List all non-expired bins. */
|
|
124
|
+
listBins(): Bin[];
|
|
125
|
+
/** Delete a bin. Returns true if the bin existed. */
|
|
126
|
+
deleteBin(id: string): boolean;
|
|
127
|
+
/**
|
|
128
|
+
* Record an incoming request against a bin. Returns the stored
|
|
129
|
+
* {@link CapturedRequest}, or `undefined` if the bin does not exist or is
|
|
130
|
+
* expired.
|
|
131
|
+
*/
|
|
132
|
+
recordRequest(binId: string, raw: Omit<CapturedRequest, "id" | "binId" | "capturedAt">): CapturedRequest | undefined;
|
|
133
|
+
/** List captured requests for a bin (newest first). */
|
|
134
|
+
getRequests(binId: string): CapturedRequest[];
|
|
135
|
+
/** Look up a single captured request. */
|
|
136
|
+
getRequest(binId: string, reqId: string): CapturedRequest | undefined;
|
|
137
|
+
/** Clear all captured requests in a bin. */
|
|
138
|
+
clearRequests(binId: string): void;
|
|
139
|
+
/** Start the periodic cleanup of expired bins. */
|
|
140
|
+
start(): void;
|
|
141
|
+
/** Stop the periodic cleanup. Idempotent. */
|
|
142
|
+
stop(): void;
|
|
143
|
+
private generateId;
|
|
144
|
+
}
|
|
145
|
+
//#endregion
|
|
5
146
|
//#region src/certificate.d.ts
|
|
6
147
|
type CertificateOptions = {
|
|
7
148
|
/**
|
|
@@ -191,6 +332,7 @@ type HttpBinOptions = {
|
|
|
191
332
|
auth?: boolean;
|
|
192
333
|
images?: boolean;
|
|
193
334
|
dynamicData?: boolean;
|
|
335
|
+
bins?: boolean;
|
|
194
336
|
};
|
|
195
337
|
type HttpsOptions = {
|
|
196
338
|
/**
|
|
@@ -282,6 +424,7 @@ declare class MockHttp extends Hookified {
|
|
|
282
424
|
private _httpsCredentials;
|
|
283
425
|
private _server;
|
|
284
426
|
private _taps;
|
|
427
|
+
private _bins;
|
|
285
428
|
constructor(options?: MockHttpOptions);
|
|
286
429
|
/**
|
|
287
430
|
* The port to listen on. If not provided, defaults to 3000 and detects the next available port if in use.
|
|
@@ -415,6 +558,14 @@ declare class MockHttp extends Hookified {
|
|
|
415
558
|
* The TapManager instance for managing injection taps.
|
|
416
559
|
*/
|
|
417
560
|
set taps(taps: TapManager);
|
|
561
|
+
/**
|
|
562
|
+
* The BinManager instance for managing request bins.
|
|
563
|
+
*/
|
|
564
|
+
get bins(): BinManager;
|
|
565
|
+
/**
|
|
566
|
+
* The BinManager instance for managing request bins.
|
|
567
|
+
*/
|
|
568
|
+
set bins(bins: BinManager);
|
|
418
569
|
/**
|
|
419
570
|
* Start the Fastify server. If the server is already running, it will be closed and restarted.
|
|
420
571
|
*/
|
|
@@ -484,6 +635,11 @@ declare class MockHttp extends Hookified {
|
|
|
484
635
|
* @param fastifyInstance - the server instance to register the routes on.
|
|
485
636
|
*/
|
|
486
637
|
registerDynamicDataRoutes(fastifyInstance?: FastifyInstance): Promise<void>;
|
|
638
|
+
/**
|
|
639
|
+
* Register the request bin routes (management at /bins and capture at /b/:id).
|
|
640
|
+
* @param fastifyInstance - the server instance to register the routes on.
|
|
641
|
+
*/
|
|
642
|
+
registerBinRoutes(fastifyInstance?: FastifyInstance): Promise<void>;
|
|
487
643
|
private resolveHttpsCredentials;
|
|
488
644
|
private loadPemValue;
|
|
489
645
|
}
|
|
@@ -491,4 +647,4 @@ declare class MockHttp extends Hookified {
|
|
|
491
647
|
//#region src/index.d.ts
|
|
492
648
|
declare const start: () => Promise<MockHttp>;
|
|
493
649
|
//#endregion
|
|
494
|
-
export { type CertificateFileOptions, type CertificateOptions, type CertificateResult, type HttpsOptions, type InjectionMatcher, type InjectionResponse, type InjectionTap, MockHttp as default, MockHttp as mockhttp, generateCertificate, generateCertificateFiles, start };
|
|
650
|
+
export { type Bin, BinManager, type BinManagerOptions, type BinStore, type CapturedRequest, type CertificateFileOptions, type CertificateOptions, type CertificateResult, type HttpsOptions, InMemoryBinStore, type InjectionMatcher, type InjectionResponse, type InjectionTap, MockHttp as default, MockHttp as mockhttp, generateCertificate, generateCertificateFiles, start };
|