@jaredwray/mockhttp 2.1.0 → 3.0.1

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.
@@ -489,6 +489,74 @@ curl http://localhost:3000/bins/abc123def456/requests
489
489
  | DELETE | `/bins/:id/requests` | Clear all captured requests in a bin |
490
490
  | DELETE | `/bins/:id` | Delete a bin |
491
491
  | ANY | `/b/:id` and `/b/:id/*` | Capture requests sent to a bin |
492
+ | GET | `/healthz` | Check whether bin storage is reachable |
493
+
494
+ ## Storage health
495
+
496
+ `GET /healthz` is on the Node server and the Worker. This route does not create a bin.
497
+
498
+ A memory store sends HTTP 200:
499
+
500
+ ```json
501
+ {
502
+ "status": "ok",
503
+ "bins": {
504
+ "store": "memory",
505
+ "connected": true,
506
+ "latencyMs": 1,
507
+ "uriPresent": false
508
+ }
509
+ }
510
+ ```
511
+
512
+ The Worker uses MongoDB when `MONGODB_URI` is set. The check opens a client, sends an admin ping, and then closes the client. The timeout is short. The check does not keep the client after a failure.
513
+
514
+ A successful ping sends HTTP 200. `store` is `mongo`.
515
+
516
+ A failed ping sends HTTP 503:
517
+
518
+ ```json
519
+ {
520
+ "status": "error",
521
+ "bins": {
522
+ "store": "mongo",
523
+ "connected": false,
524
+ "uriPresent": true,
525
+ "uri": {
526
+ "length": 78,
527
+ "leadingWhitespace": false,
528
+ "trailingWhitespace": false,
529
+ "newline": false,
530
+ "wrappedQuotes": false,
531
+ "atCount": 2,
532
+ "passwordReserved": ["@"],
533
+ "passwordHasPercent": false
534
+ },
535
+ "error": {
536
+ "name": "MongoParseError",
537
+ "message": "Invalid connection string. Percent-encode reserved characters in the username and password (@ : / ? # [ ] $).",
538
+ "code": null
539
+ }
540
+ }
541
+ }
542
+ ```
543
+
544
+ A MongoDB result includes `bins.uri` on success and on failure. A memory result omits `bins.uri`. The response does not include the scheme, the host, the username, or the password length.
545
+
546
+ `uri.passwordReserved` lists raw reserved characters in the password. The list is empty when the password has only reserved characters. `uri.passwordHasPercent` is true when the password contains `%`. The response does not contain the connection string or the password. The logs do not contain the connection string, the username, the password, or the host. `uriPresent` is a boolean.
547
+
548
+ `Invalid connection string` is the driver text. MockHTTP adds the percent-encode hint only for that text or for `unescaped characters`. A DNS error stays a DNS error. `Invalid connection string` means that the URI has a second `@`. It also means that you did not encode a reserved character in the username or the password. Encode these characters:
549
+
550
+ - `@` as `%40`
551
+ - `:` as `%3A`
552
+ - `/` as `%2F`
553
+ - `?` as `%3F`
554
+ - `#` as `%23`
555
+ - `[` as `%5B`
556
+ - `]` as `%5D`
557
+ - `$` as `%24`
558
+
559
+ A correct `mongodb+srv://` URI does not fail at parse time. A DNS failure or a network failure has a different error name.
492
560
 
493
561
  ## End-to-End Webhook Debugging Example
494
562
 
@@ -617,13 +685,13 @@ curl -s "http://localhost:3000/bins/$ID/requests" \
617
685
 
618
686
  ### Truncation
619
687
 
620
- Bodies larger than `maxBodySize` (1 MiB by default) are cut off and flagged:
688
+ Bodies larger than `maxBodySize` (128 KiB by default) are cut off and flagged:
621
689
 
622
690
  ```javascript
623
691
  // In test setup
624
- import { MockHttp, BinManager } from '@jaredwray/mockhttp';
692
+ import { MockHttp, MockHttpBins } from '@jaredwray/mockhttp';
625
693
  const mock = new MockHttp();
626
- mock.bins = new BinManager({ maxBodySize: 1024 }); // 1 KiB cap
694
+ mock.bins = new MockHttpBins({ maxBodySize: 1024 }); // 1 KiB cap
627
695
  await mock.start();
628
696
  ```
629
697
 
@@ -663,8 +731,7 @@ curl -s "http://localhost:3000/bins/$ID/requests" | jq '.requests[0].path'
663
731
 
664
732
  ## Programmatic Access
665
733
 
666
- The bin manager is exposed on the `MockHttp` instance as `mock.bins`. This
667
- makes it easy to drive bins from a test suite without going through HTTP:
734
+ Use `mock.bins` to control request bins from a test. You do not need an HTTP call:
668
735
 
669
736
  ```javascript
670
737
  import { MockHttp } from '@jaredwray/mockhttp';
@@ -673,13 +740,13 @@ const mock = new MockHttp({ logging: false });
673
740
  await mock.start();
674
741
 
675
742
  // Create a bin
676
- const bin = mock.bins.createBin();
743
+ const bin = await mock.bins.createBin();
677
744
  console.log(`Webhook target: http://localhost:${mock.port}/b/${bin.id}`);
678
745
 
679
746
  // ... point your code-under-test at that URL ...
680
747
 
681
748
  // Read captured requests (newest first)
682
- const requests = mock.bins.getRequests(bin.id);
749
+ const requests = await mock.bins.getRequests(bin.id);
683
750
  console.log(`Captured ${requests.length} requests`);
684
751
 
685
752
  for (const req of requests) {
@@ -687,8 +754,8 @@ for (const req of requests) {
687
754
  }
688
755
 
689
756
  // Clean up
690
- mock.bins.deleteBin(bin.id);
691
- await mock.close(); // also stops the bin cleanup timer
757
+ await mock.bins.deleteBin(bin.id);
758
+ await mock.close();
692
759
  ```
693
760
 
694
761
  ### Using Bins in a Vitest Test
@@ -709,14 +776,14 @@ afterAll(async () => {
709
776
  });
710
777
 
711
778
  test('my SDK sends the right webhook payload', async () => {
712
- const bin = mock.bins.createBin();
779
+ const bin = await mock.bins.createBin();
713
780
  const webhookUrl = `http://localhost:${mock.port}/b/${bin.id}`;
714
781
 
715
782
  // Drive your SDK at the bin
716
783
  await mySdk.notify(webhookUrl, { event: 'user.created', id: 42 });
717
784
 
718
785
  // Assert on what actually arrived
719
- const [captured] = mock.bins.getRequests(bin.id);
786
+ const [captured] = await mock.bins.getRequests(bin.id);
720
787
  expect(captured.method).toBe('POST');
721
788
  expect(captured.contentType).toBe('application/json');
722
789
  expect(JSON.parse(captured.body!)).toEqual({
@@ -737,18 +804,17 @@ const mock = new MockHttp({
737
804
  });
738
805
  ```
739
806
 
740
- To tune limits, replace the default `BinManager` before starting:
807
+ To change the limits, replace the default `MockHttpBins` before you start the server:
741
808
 
742
809
  ```javascript
743
- import { MockHttp, BinManager } from '@jaredwray/mockhttp';
810
+ import { MockHttp, MockHttpBins } from '@jaredwray/mockhttp';
744
811
 
745
812
  const mock = new MockHttp();
746
- mock.bins = new BinManager({
813
+ mock.bins = new MockHttpBins({
747
814
  defaultTtlMs: 60 * 60 * 1000, // 1 hour (default: 24h)
748
815
  maxRequestsPerBin: 500, // (default: 100)
749
- maxBodySize: 5 * 1024 * 1024, // 5 MiB (default: 1 MiB)
816
+ maxBodySize: 512 * 1024, // 512 KiB (default: 128 KiB)
750
817
  idLength: 16, // (default: 12)
751
- cleanupIntervalMs: 30 * 1000, // sweep every 30s (default: 60s)
752
818
  });
753
819
 
754
820
  await mock.start();
@@ -758,44 +824,40 @@ await mock.start();
758
824
 
759
825
  | Option | Default | Description |
760
826
  |--------|---------|-------------|
761
- | `defaultTtlMs` | `86400000` (24h) | Bin lifetime. Expired bins return 404 and are lazily removed. |
827
+ | `defaultTtlMs` | `86400000` (24h) | Bin lifetime, set as the Keyv TTL. An expired bin returns 404. |
762
828
  | `maxRequestsPerBin` | `100` | When exceeded, oldest captures are dropped (FIFO). |
763
- | `maxBodySize` | `1048576` (1 MiB) | Larger bodies are truncated; `truncated: true` is set on the capture. |
829
+ | `maxBodySize` | `131072` (128 KiB) | Larger bodies are truncated; `truncated: true` is set on the capture. |
764
830
  | `idLength` | `12` | Length of generated bin and request ids. |
765
- | `cleanupIntervalMs` | `60000` (1 min) | How often expired bins are swept. The timer is `unref()`'d so it never keeps the process alive. |
831
+ | `keyv` | `new Keyv()` | Keyv store for bins. The default keeps bins in memory. |
832
+
833
+ ### Storage
766
834
 
767
- ### Pluggable Storage
835
+ `MockHttpBins` stores each bin in [Keyv](https://github.com/jaredwray/keyv) 6. The default is `new Keyv()`. That store keeps the data in memory for this process.
768
836
 
769
- `BinManager` accepts a `store: BinStore` so you can plug in alternative
770
- backends (Redis, SQLite, etc.) without changing the rest of the codebase. The
771
- default `InMemoryBinStore` keeps state in process memory.
837
+ Pass a Keyv 6 instance when you want a different adapter. Install the adapter in your project. This package does not include an adapter. `@keyv/redis` 6 matches this Keyv version.
772
838
 
773
839
  ```typescript
774
- import {
775
- BinManager,
776
- InMemoryBinStore,
777
- type Bin,
778
- type BinStore,
779
- type CapturedRequest,
780
- } from '@jaredwray/mockhttp';
781
-
782
- class RedisBinStore implements BinStore {
783
- createBin(bin: Bin): void { /* SET bin:${bin.id} ... */ }
784
- getBin(id: string): Bin | undefined { /* GET ... */ }
785
- listBins(): Bin[] { /* SCAN ... */ }
786
- deleteBin(id: string): boolean { /* DEL ... */ }
787
- addRequest(binId: string, req: CapturedRequest, max: number): void {
788
- /* LPUSH bin:${binId}:requests + LTRIM to max */
789
- }
790
- getRequests(binId: string): CapturedRequest[] { /* LRANGE ... */ }
791
- getRequest(binId: string, reqId: string): CapturedRequest | undefined { /* ... */ }
792
- clearRequests(binId: string): void { /* DEL bin:${binId}:requests */ }
793
- cleanupExpired(now: number): string[] { /* scan + delete */ return []; }
794
- }
840
+ import Keyv from 'keyv';
841
+ import KeyvRedis from '@keyv/redis'; // pnpm add @keyv/redis
842
+ import { MockHttpBins } from '@jaredwray/mockhttp';
795
843
 
796
- mock.bins = new BinManager({ store: new RedisBinStore() });
844
+ mock.bins = new MockHttpBins({
845
+ keyv: new Keyv({ store: new KeyvRedis('redis://localhost:6379') }),
846
+ });
797
847
  ```
798
848
 
849
+ Each capture writes the full bin again. A large body or a high request limit makes that write large. Set `maxBodySize` and `maxRequestsPerBin` to limit the size. With the defaults, a full bin of binary bodies is about 17 MB.
850
+
851
+ Two processes that share one Keyv can lose a capture. This occurs when both processes write the same bin at the same time.
852
+
853
+ If Keyv does not store a write, the bin route returns `500` with the standard Fastify error body: `statusCode`, `error`, and `message`.
854
+
855
+ A failed read depends on the Keyv `error` listener. Without a listener, Keyv throws the store error and the route returns `500`. With a listener, Keyv does not throw. A failed read then looks like a missing bin, and the route returns `404`. `listBins()` returns only the bins that it read before the error.
856
+
857
+ `listBins()` throws an error when the adapter has no iterator.
858
+
859
+ An expired bin returns 404. The in-memory Keyv store deletes an expired bin only when a read or `listBins()` reaches it. Until then, the bin stays in memory.
860
+
799
861
  ### Configuration
800
862
  URL: https://mockhttp.org/docs/configuration/
801
863
 
@@ -1017,7 +1079,6 @@ new MockHttp(options?)
1017
1079
  - `logging?`: boolean - Enable logging (default: true)
1018
1080
  - `staticFiles?`: boolean - Serve files from the package `public/` directory (default: true)
1019
1081
  - `pluginTimeout?`: number - Fastify plugin boot timeout in milliseconds; `0` disables it (default: Fastify's timeout)
1020
- - `startBins?`: boolean - Start the request-bin cleanup timer during `initialize()` (default: true)
1021
1082
  - `httpBin?`: HttpBinOptions - Configure which httpbin routes to enable
1022
1083
  - `httpMethods?`: boolean - Enable HTTP method routes (default: true)
1023
1084
  - `redirects?`: boolean - Enable redirect routes (default: true)
@@ -1046,7 +1107,6 @@ new MockHttp(options?)
1046
1107
  - `logging`: boolean - Get/set logging enabled state
1047
1108
  - `staticFiles`: boolean - Get/set whether files from `public/` are served
1048
1109
  - `pluginTimeout`: number | undefined - Get/set Fastify plugin boot timeout
1049
- - `startBins`: boolean - Get/set whether bin cleanup starts during `initialize()`
1050
1110
  - `rateLimit`: RateLimitPluginOptions | undefined - Get/set rate limiting options
1051
1111
  - `httpBin`: HttpBinOptions - Get/set httpbin route options
1052
1112
  - `https`: HttpsOptions | undefined - Get/set HTTPS configuration
@@ -1055,7 +1115,7 @@ new MockHttp(options?)
1055
1115
  - `http1`: boolean - Get/set HTTP/1.1 fallback for HTTP/2 with HTTPS
1056
1116
  - `server`: FastifyInstance - Get/set the Fastify server instance
1057
1117
  - `taps`: TapManager - Get/set the TapManager instance
1058
- - `bins`: BinManager - Get/set the BinManager instance for request bins
1118
+ - `bins`: MockHttpBins - Get or set the request bins
1059
1119
 
1060
1120
  ### Methods
1061
1121
 
@@ -1131,61 +1191,59 @@ Register image routes (jpeg, png, svg, webp) with content negotiation support.
1131
1191
 
1132
1192
  #### `async registerBinRoutes(fastifyInstance?)`
1133
1193
 
1134
- Register the request bin routes — management at `/bins` and capture at `/b/:id`.
1194
+ Register the request bin routes. Management is at `/bins` and capture is at `/b/:id`. `GET /healthz` reports whether bin storage is reachable.
1135
1195
 
1136
1196
  ## Bins (Request Capture)
1137
1197
 
1138
- Access the BinManager via `mockHttp.bins` to manage request bins programmatically. See [Request Bins](#request-bins) for usage examples.
1198
+ Use `mockHttp.bins` to control request bins. See [Request Bins](#request-bins) for usage examples.
1139
1199
 
1140
1200
  ### `bins.createBin()`
1141
1201
 
1142
- Create a new bin with the configured TTL.
1202
+ Create a new bin with the configured TTL. Throws when Keyv does not store the bin.
1143
1203
 
1144
- **Returns:** `Bin` — `{ id, createdAt, expiresAt, requestCount }`
1204
+ **Returns:** `Promise<Bin>` — `{ id, createdAt, expiresAt, requestCount }`
1145
1205
 
1146
1206
  ### `bins.getBin(id)`
1147
1207
 
1148
- Look up a bin by id. Returns `undefined` if the bin does not exist or has expired (and lazily removes the expired entry).
1208
+ Look up a bin by id. Returns `undefined` if the bin does not exist or Keyv has expired it. When the Keyv instance has an `error` listener, a failed read also returns `undefined`.
1149
1209
 
1150
- **Returns:** `Bin | undefined`
1210
+ **Returns:** `Promise<Bin | undefined>`
1151
1211
 
1152
1212
  ### `bins.listBins()`
1153
1213
 
1154
- List all non-expired bins.
1214
+ List the bins that Keyv still holds.
1155
1215
 
1156
- **Returns:** `Bin[]`
1216
+ **Returns:** `Promise<Bin[]>`
1157
1217
 
1158
1218
  ### `bins.deleteBin(id)`
1159
1219
 
1160
- Delete a bin and all its captured requests.
1220
+ Delete a bin and all its captured requests. The delete waits for captures already queued for that bin.
1161
1221
 
1162
- **Returns:** `boolean` — `true` if the bin existed
1222
+ **Returns:** `Promise<boolean>` — `true` when the bin existed
1163
1223
 
1164
1224
  ### `bins.getRequests(binId)`
1165
1225
 
1166
1226
  List captured requests for a bin, newest first.
1167
1227
 
1168
- **Returns:** `CapturedRequest[]`
1228
+ **Returns:** `Promise<CapturedRequest[]>`
1169
1229
 
1170
1230
  ### `bins.getRequest(binId, reqId)`
1171
1231
 
1172
1232
  Look up a single captured request.
1173
1233
 
1174
- **Returns:** `CapturedRequest | undefined`
1234
+ **Returns:** `Promise<CapturedRequest | undefined>`
1175
1235
 
1176
1236
  ### `bins.clearRequests(binId)`
1177
1237
 
1178
- Clear all captured requests in a bin (the bin itself is preserved).
1238
+ Clear all captured requests in a bin. The bin stays. Throws when Keyv does not store the change.
1179
1239
 
1180
- ### `bins.recordRequest(binId, raw)`
1181
-
1182
- 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.
1240
+ **Returns:** `Promise<boolean>` — `false` when the bin does not exist
1183
1241
 
1184
- **Returns:** `CapturedRequest | undefined`
1242
+ ### `bins.recordRequest(binId, raw)`
1185
1243
 
1186
- ### `bins.start()` / `bins.stop()`
1244
+ Manually record a captured request. Returns `undefined` if the bin does not exist. Throws when Keyv does not store the capture. Normally invoked by the capture route, but available for programmatic use.
1187
1245
 
1188
- 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.
1246
+ **Returns:** `Promise<CapturedRequest | undefined>`
1189
1247
 
1190
1248
  ### `bins.maxBodySize` / `bins.maxRequestsPerBin` / `bins.defaultTtlMs`
1191
1249
 
@@ -1248,7 +1306,7 @@ URL: https://mockhttp.org/docs/hosted/
1248
1306
 
1249
1307
  # About mockhttp.org
1250
1308
 
1251
- [mockhttp.org](https://mockhttp.org) is a free hosted instance of this codebase for testing. It runs on a [Cloudflare Worker](/docs/how-to-deploy/cloudflare/) (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.
1309
+ [mockhttp.org](https://mockhttp.org) is a free hosted instance of this codebase for testing. It runs on a [Cloudflare Worker](/docs/how-to-deploy/cloudflare/) (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. Request bins on this service are stored in MongoDB Atlas, in the `bins` collection, so they are shared across isolates.
1252
1310
 
1253
1311
  All [mockhttp.org](https://mockhttp.org) traffic stays on Cloudflare. A separate test app runs the Node server on [Wasmer Edge](/docs/how-to-deploy/wasmer/) at [mockhttp.wasmer.app](https://mockhttp.wasmer.app).
1254
1312
 
@@ -1273,12 +1331,21 @@ The Worker starts the same MockHTTP app as Node.js, with a few options set for t
1273
1331
  | `rateLimit` | `false` | [`@fastify/rate-limit`](https://github.com/fastify/fastify-rate-limit) is off. A Cloudflare rate limiting binding allows 1000 requests per 60 seconds per IP. |
1274
1332
  | `siteDistPath` | unused path | Fastify does not serve the docs. Workers Assets serve `site/dist`. |
1275
1333
  | `staticFiles` | `false` | Logos and other `public/` files are copied into `site/dist` and read through the `ASSETS` binding. |
1276
- | `startBins` | `false` | The bin cleanup timer uses `setInterval`, which can hang a Worker isolate during startup. Bin routes still register. Expired bins are removed when that bin is read, not by a sweep of every bin. |
1277
1334
  | `pluginTimeout` | `0` | Disables Fastify's plugin boot timeout. Those timers are unreliable on Workers. |
1278
1335
  | `logging` | `false` | Request logging is off. |
1279
1336
  | `autoDetectPort` | `false` | The app always listens on port 3000 inside the isolate. |
1280
1337
 
1281
- The rate limiter key is the `cf-connecting-ip` header, then the first address in `x-forwarded-for`, then `unknown`. A limited request returns `429` with `retry-after: 60`. That limiter is shared across isolates. Bins are not. The default store is in memory inside the isolate that handled the request, so a bin created on one request can be missing when another isolate or location handles the next one.
1338
+ The rate limiter key is the `cf-connecting-ip` header, then the first address in `x-forwarded-for`, then `unknown`. A limited request returns `429` with `retry-after: 60`. That limiter is shared across isolates.
1339
+
1340
+ Bins stay in memory in the isolate when `MONGODB_URI` is not set. A bin from one request can be absent when a different isolate handles the next request. When `MONGODB_URI` is set, bins are stored in MongoDB. Each request opens its own client on its first bin operation and closes it after the response, through `ctx.waitUntil`. No client is shared between requests: workerd rejects I/O on a socket that a different request opened, and a shared client made bin requests fail with error 1101. It does not connect while the isolate starts. Each bin request therefore pays one connection setup, which is a few round trips to Atlas.
1341
+
1342
+ Percent-encode reserved characters in the username and the password. `@` is `%40`. An unencoded `@` makes the driver throw `Invalid connection string` before it contacts Atlas.
1343
+
1344
+ `GET /healthz` reports the bin store. A successful admin ping returns HTTP 200 and `bins.connected: true`. A failed ping returns HTTP 503 with the driver error name, a redacted message, and a code. The body has `uriPresent` (boolean). It does not have the scheme. On the Node server, this route reports the memory store.
1345
+
1346
+ When `MONGODB_URI` is set, `bins.uri` has the URI length and the `@` count. It also has flags for whitespace, a newline, and quote marks. It does not have the host, the username, or the password length. The body does not have the password or the full URI.
1347
+
1348
+ The percent-encode hint is added only for `Invalid connection string` or `unescaped characters`. A DNS error keeps the driver text. `uri.passwordReserved` is empty when the password has only reserved characters. Worker logs still omit the URI, the username, the password, and the host.
1282
1349
 
1283
1350
  Taps are a library API (`mock.taps.inject`). They have no HTTP routes, so a deployed Worker has no way to inject one.
1284
1351
 
@@ -1422,7 +1489,7 @@ Change `name` and `owner` before you deploy your own app. The environment variab
1422
1489
 
1423
1490
  [`Anybuild`](https://github.com/jaredwray/mockhttp/blob/main/Anybuild) tells Wasmer how to build the package on a remote builder:
1424
1491
 
1425
- - Node.js 24 and pnpm `12.4.1`
1492
+ - Node.js 24 and pnpm `12.12.1`
1426
1493
  - Build command: `pnpm run build && pnpm website:build`
1427
1494
  - Start command: `node dist/index.mjs`
1428
1495
 
@@ -1448,7 +1515,7 @@ wasmer deploy --non-interactive --build-remote
1448
1515
 
1449
1516
  ## Runtime behavior
1450
1517
 
1451
- Bins use the default in-memory store, so a bin exists only on the instance that created it. Taps are a library API (`mock.taps.inject`) and have no HTTP routes, so a deployed app cannot inject one.
1518
+ Bins use `new Keyv()` by default. The store keeps each bin in memory on the instance that created it. Give `MockHttpBins` a shared Keyv when bins must be visible on more than one instance. Two processes can lose a capture when they write the same bin at the same time. Taps are a library API (`mock.taps.inject`) and have no HTTP routes, so a deployed app cannot inject one.
1452
1519
 
1453
1520
  `@fastify/rate-limit` is on at 1000 requests per minute. It keys on the address that connects to the process. The server does not trust proxy headers, and the allow list is only `127.0.0.1` and `::1`. On the test app, `/ip` reports `127.0.0.100`, which is the edge proxy rather than the caller, and the response includes `x-ratelimit-limit: 1000`. Clients that share that proxy address share one limit on that instance. This is not the Cloudflare limiter used by [mockhttp.org](https://mockhttp.org).
1454
1521
 
@@ -1464,12 +1531,237 @@ OpenAPI Spec Source: /home/runner/work/mockhttp/mockhttp/site/api/swagger.json
1464
1531
  "info": {
1465
1532
  "title": "Mock HTTP API",
1466
1533
  "description": "\nA simple HTTP server that can be used to mock HTTP responses for testing purposes. Inspired by [httpbin](https://httpbin.org/) and built using `nodejs` and `fastify` with the idea of running it via https://mockhttp.org, via docker `jaredwray/mockhttp`, or nodejs `npm install @jaredwray/mockhttp`.\n\n* [GitHub Repository](https://github.com/jaredwray/mockhttp)\n* [Docker Image](https://hub.docker.com/r/jaredwray/mockhttp)\n* [NPM Package](https://www.npmjs.com/package/@jaredwray/mockhttp)\n\n# About mockhttp.org\n\n[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.\n",
1467
- "version": "2.1.0"
1534
+ "version": "3.0.1"
1468
1535
  },
1469
1536
  "components": {
1470
1537
  "schemas": {}
1471
1538
  },
1472
1539
  "paths": {
1540
+ "/healthz": {
1541
+ "get": {
1542
+ "tags": [
1543
+ "Health"
1544
+ ],
1545
+ "description": "Report whether bin storage is reachable. MongoDB failures include the driver error. The scheme, the host, the username, the password, the password length, and the full URI are never returned.",
1546
+ "responses": {
1547
+ "200": {
1548
+ "description": "Default Response",
1549
+ "content": {
1550
+ "application/json": {
1551
+ "schema": {
1552
+ "type": "object",
1553
+ "properties": {
1554
+ "status": {
1555
+ "type": "string",
1556
+ "enum": [
1557
+ "ok"
1558
+ ]
1559
+ },
1560
+ "bins": {
1561
+ "type": "object",
1562
+ "properties": {
1563
+ "store": {
1564
+ "type": "string",
1565
+ "enum": [
1566
+ "mongo",
1567
+ "memory"
1568
+ ]
1569
+ },
1570
+ "connected": {
1571
+ "type": "boolean"
1572
+ },
1573
+ "latencyMs": {
1574
+ "type": "integer"
1575
+ },
1576
+ "uriPresent": {
1577
+ "type": "boolean"
1578
+ },
1579
+ "uri": {
1580
+ "type": "object",
1581
+ "properties": {
1582
+ "length": {
1583
+ "type": "integer"
1584
+ },
1585
+ "leadingWhitespace": {
1586
+ "type": "boolean"
1587
+ },
1588
+ "trailingWhitespace": {
1589
+ "type": "boolean"
1590
+ },
1591
+ "newline": {
1592
+ "type": "boolean"
1593
+ },
1594
+ "wrappedQuotes": {
1595
+ "type": "boolean"
1596
+ },
1597
+ "atCount": {
1598
+ "type": "integer"
1599
+ },
1600
+ "passwordReserved": {
1601
+ "type": "array",
1602
+ "items": {
1603
+ "type": "string"
1604
+ }
1605
+ },
1606
+ "passwordHasPercent": {
1607
+ "type": "boolean"
1608
+ }
1609
+ },
1610
+ "required": [
1611
+ "length",
1612
+ "leadingWhitespace",
1613
+ "trailingWhitespace",
1614
+ "newline",
1615
+ "wrappedQuotes",
1616
+ "atCount",
1617
+ "passwordReserved",
1618
+ "passwordHasPercent"
1619
+ ]
1620
+ }
1621
+ },
1622
+ "required": [
1623
+ "store",
1624
+ "connected",
1625
+ "latencyMs",
1626
+ "uriPresent"
1627
+ ]
1628
+ }
1629
+ },
1630
+ "required": [
1631
+ "status",
1632
+ "bins"
1633
+ ]
1634
+ }
1635
+ }
1636
+ }
1637
+ },
1638
+ "503": {
1639
+ "description": "Default Response",
1640
+ "content": {
1641
+ "application/json": {
1642
+ "schema": {
1643
+ "type": "object",
1644
+ "properties": {
1645
+ "status": {
1646
+ "type": "string",
1647
+ "enum": [
1648
+ "error"
1649
+ ]
1650
+ },
1651
+ "bins": {
1652
+ "type": "object",
1653
+ "properties": {
1654
+ "store": {
1655
+ "type": "string",
1656
+ "enum": [
1657
+ "mongo",
1658
+ "memory"
1659
+ ]
1660
+ },
1661
+ "connected": {
1662
+ "type": "boolean"
1663
+ },
1664
+ "latencyMs": {
1665
+ "type": "integer"
1666
+ },
1667
+ "uriPresent": {
1668
+ "type": "boolean"
1669
+ },
1670
+ "uri": {
1671
+ "type": "object",
1672
+ "properties": {
1673
+ "length": {
1674
+ "type": "integer"
1675
+ },
1676
+ "leadingWhitespace": {
1677
+ "type": "boolean"
1678
+ },
1679
+ "trailingWhitespace": {
1680
+ "type": "boolean"
1681
+ },
1682
+ "newline": {
1683
+ "type": "boolean"
1684
+ },
1685
+ "wrappedQuotes": {
1686
+ "type": "boolean"
1687
+ },
1688
+ "atCount": {
1689
+ "type": "integer"
1690
+ },
1691
+ "passwordReserved": {
1692
+ "type": "array",
1693
+ "items": {
1694
+ "type": "string"
1695
+ }
1696
+ },
1697
+ "passwordHasPercent": {
1698
+ "type": "boolean"
1699
+ }
1700
+ },
1701
+ "required": [
1702
+ "length",
1703
+ "leadingWhitespace",
1704
+ "trailingWhitespace",
1705
+ "newline",
1706
+ "wrappedQuotes",
1707
+ "atCount",
1708
+ "passwordReserved",
1709
+ "passwordHasPercent"
1710
+ ]
1711
+ },
1712
+ "error": {
1713
+ "type": "object",
1714
+ "properties": {
1715
+ "name": {
1716
+ "type": "string"
1717
+ },
1718
+ "message": {
1719
+ "type": "string"
1720
+ },
1721
+ "code": {
1722
+ "anyOf": [
1723
+ {
1724
+ "type": "string"
1725
+ },
1726
+ {
1727
+ "type": "number"
1728
+ },
1729
+ {
1730
+ "type": "object",
1731
+ "nullable": true,
1732
+ "enum": [
1733
+ null
1734
+ ]
1735
+ }
1736
+ ]
1737
+ }
1738
+ },
1739
+ "required": [
1740
+ "name",
1741
+ "message",
1742
+ "code"
1743
+ ]
1744
+ }
1745
+ },
1746
+ "required": [
1747
+ "store",
1748
+ "connected",
1749
+ "uriPresent",
1750
+ "error"
1751
+ ]
1752
+ }
1753
+ },
1754
+ "required": [
1755
+ "status",
1756
+ "bins"
1757
+ ]
1758
+ }
1759
+ }
1760
+ }
1761
+ }
1762
+ }
1763
+ }
1764
+ },
1473
1765
  "/get": {
1474
1766
  "get": {
1475
1767
  "tags": [
@@ -1598,7 +1890,11 @@ OpenAPI Spec Source: /home/runner/work/mockhttp/mockhttp/site/api/swagger.json
1598
1890
  "type": "string"
1599
1891
  },
1600
1892
  {
1601
- "type": "null"
1893
+ "type": "object",
1894
+ "nullable": true,
1895
+ "enum": [
1896
+ null
1897
+ ]
1602
1898
  }
1603
1899
  ]
1604
1900
  }
@@ -5840,10 +6136,8 @@ OpenAPI Spec Source: /home/runner/work/mockhttp/mockhttp/site/api/swagger.json
5840
6136
  "type": "integer"
5841
6137
  },
5842
6138
  "body": {
5843
- "type": [
5844
- "null",
5845
- "string"
5846
- ]
6139
+ "type": "string",
6140
+ "nullable": true
5847
6141
  },
5848
6142
  "bodyEncoding": {
5849
6143
  "type": "string",
@@ -6010,10 +6304,8 @@ OpenAPI Spec Source: /home/runner/work/mockhttp/mockhttp/site/api/swagger.json
6010
6304
  "type": "integer"
6011
6305
  },
6012
6306
  "body": {
6013
- "type": [
6014
- "null",
6015
- "string"
6016
- ]
6307
+ "type": "string",
6308
+ "nullable": true
6017
6309
  },
6018
6310
  "bodyEncoding": {
6019
6311
  "type": "string",
@@ -6950,6 +7242,143 @@ OpenAPI Spec Source: /home/runner/work/mockhttp/mockhttp/site/api/swagger.json
6950
7242
  ## Changelog
6951
7243
  URL: https://mockhttp.org/changelog
6952
7244
 
7245
+ ### v3.0.1
7246
+ URL: https://mockhttp.org/changelog/v3-0-1
7247
+ Date: October 28, 2026
7248
+ Tag: Release
7249
+
7250
+ ## @jaredwray/mockhttp@3.0.1 — 2026-10-28
7251
+
7252
+ Security patches and Worker fixes. /healthz omits scheme, host, username, and password length.
7253
+
7254
+ ### Bug Fixes
7255
+ - start the Worker during isolate startup (#248)
7256
+ - report bin store health and keep a failed connect in process (#249)
7257
+ - drop every second socket error that has no listener (#250)
7258
+ - Add redacted URI diagnostics to /healthz (#254)
7259
+ - open one MongoDB client per Worker request to stop 1101s on bins (#255)
7260
+ - pin Node 24.22.1 and patched undici (#256)
7261
+ - force miniflare's sharp pin to 0.35.5 (#257)
7262
+ - bump zod to 4.6.7 for GHSA-mrhx-c7xc-m3w5 (#258)
7263
+ - security - markdown-it 15.0.3 (CVE-2026-111758) (#260)
7264
+ - omit scheme and URI identity from /healthz (#262)
7265
+
7266
+ ```json
7267
+ {
7268
+ "status": "ok",
7269
+ "bins": {
7270
+ "connected": true,
7271
+ "uriPresent": true,
7272
+ "uri": {
7273
+ "atCount": 1,
7274
+ "passwordHasPercent": false
7275
+ }
7276
+ }
7277
+ }
7278
+ ```
7279
+
7280
+ ### Documentation
7281
+ - remove the empty vars note from the Cloudflare page (#253)
7282
+
7283
+ ### Internal
7284
+ - point worker URI fixtures at example.invalid (#251)
7285
+ - declare empty Worker vars in Wrangler (#252)
7286
+ - list LICENSE in the published package files (#259)
7287
+
7288
+ ### Contributors
7289
+ - @jaredwray (13)
7290
+ - @cursoragent (1)
7291
+
7292
+ ### Full List of Changes
7293
+ - fix: restore Cloudflare Worker routes after the v3.0.0 startup crash by @jaredwray in #248
7294
+ - Add GET /healthz and redact bin store errors by @jaredwray in #249
7295
+ - Use example.invalid for worker test URI fixtures by @jaredwray in #251
7296
+ - Drop every second socket error that has no listener by @jaredwray in #250
7297
+ - Declare empty Worker vars in Wrangler by @jaredwray in #252
7298
+ - Remove the empty vars note from the Cloudflare page by @jaredwray in #253
7299
+ - Add redacted URI diagnostics to /healthz by @jaredwray in #254
7300
+ - fix: open one MongoDB client per Worker request to stop 1101s on bins by @jaredwray in #255
7301
+ - security: pin Node 24.22.1 and patched undici by @jaredwray in #256
7302
+ - fix: security - upgrade sharp to 0.35.5 (libheif RCE) by @jaredwray in #257
7303
+ - fix: security - bump zod to 4.6.7 (GHSA-mrhx-c7xc-m3w5) by @jaredwray in #258
7304
+ - chore: list LICENSE in the published package files by @jaredwray in #259
7305
+ - fix: security - markdown-it 15.0.3 (CVE-2026-111758) by @jaredwray in #260
7306
+ - fix: omit scheme and URI identity from /healthz by @jaredwray in #262
7307
+
7308
+ **Full diff:** [https://github.com/jaredwray/mockhttp/compare/v3.0.0...v3.0.1](https://github.com/jaredwray/mockhttp/compare/v3.0.0...v3.0.1)
7309
+
7310
+ ### v3.0.0
7311
+ URL: https://mockhttp.org/changelog/v3-0-0
7312
+ Date: October 20, 2026
7313
+ Tag: Release
7314
+
7315
+ ## @jaredwray/mockhttp@3.0.0 — 2026-10-19
7316
+
7317
+ Request bins now use Keyv via MockHttpBins, and the Worker can store them in MongoDB Atlas.
7318
+
7319
+ ### ⚠ BREAKING CHANGES
7320
+ - Replace `BinManager`, `BinStore`, and `InMemoryBinStore` with async `MockHttpBins`. `mock.bins.start()` and `mock.bins.stop()` are removed. The default `maxBodySize` is 128 KiB (was 1 MiB). Node engines are now `>=22.19.0` (was `>=22.18.0`). (`b1ddcc0`, #246)
7321
+ Migration: assign `mock.bins = new MockHttpBins({ keyv })` before `start()` (default store is `new Keyv()`) and `await` the bin methods.
7322
+
7323
+ ```ts
7324
+ import Keyv from "keyv";
7325
+ import { MockHttp, MockHttpBins } from "@jaredwray/mockhttp";
7326
+
7327
+ const mock = new MockHttp();
7328
+ mock.bins = new MockHttpBins({ keyv: new Keyv(), maxBodySize: 128 * 1024 });
7329
+ await mock.start();
7330
+ const bin = await mock.bins.createBin();
7331
+ ```
7332
+
7333
+ ### Features
7334
+ - Store Worker request bins in MongoDB Atlas when `MONGODB_URI` is set (`266157b`, #247)
7335
+
7336
+ ```bash
7337
+ # Cloudflare Worker secret. Bins go in the Atlas `bins` collection.
7338
+ # Leave MONGODB_URI unset to keep the in-memory Keyv.
7339
+ wrangler secret put MONGODB_URI
7340
+ ```
7341
+
7342
+ ### Bug Fixes
7343
+ - security - upgrade fastify proxy-addr, fast-uri, find-my-way, undici (`9c52869`, #233)
7344
+ - security - upgrade handlebars, source-map-js (`28aa662`, #236)
7345
+
7346
+ ### Internal
7347
+ - sync defense-in-depth files from agentic (`7fb3ce4`, #234)
7348
+ - upgrade code quality dependencies (`a57b990`, #235)
7349
+ - upgrade TypeScript and build tooling (`b11fa0f`, #237)
7350
+ - upgrade monorepo tooling (`58e8596`, #238)
7351
+ - upgrade GitHub Actions (`58bc448`, #239)
7352
+ - upgrade Dev Container images (`2047c3c`, #240)
7353
+ - upgrade Fastify dependencies (`3e93bea`, #241)
7354
+ - upgrade hookified (`d938e6e`, #242)
7355
+ - upgrade pino and pino-pretty (`c2a94ad`, #243)
7356
+ - upgrade Docker Node.js runtime image (`cd5decf`, #244)
7357
+ - pin the Compose image (`74ad4ea`, #245)
7358
+
7359
+ ### Contributors
7360
+ - @jaredwray (11)
7361
+ - @cursoragent (5)
7362
+
7363
+ ### Full List of Changes
7364
+ - fix: security - upgrade fastify proxy-addr, fast-uri, find-my-way, undici by @jaredwray in #233
7365
+ - mono - chore: sync defense-in-depth files from agentic by @jaredwray in #234
7366
+ - mono - chore: upgrade code quality dependencies by @jaredwray in #235
7367
+ - fix: security - upgrade handlebars, source-map-js by @jaredwray in #236
7368
+ - mono - chore: upgrade TypeScript and build tooling by @jaredwray in #237
7369
+ - mono - chore: upgrade monorepo tooling by @jaredwray in #238
7370
+ - mono - chore: upgrade GitHub Actions by @jaredwray in #239
7371
+ - mono - chore: upgrade Dev Container images by @jaredwray in #240
7372
+ - mono - chore: upgrade Fastify dependencies by @jaredwray in #241
7373
+ - mono - chore: upgrade hookified by @jaredwray in #242
7374
+ - mono - chore: upgrade pino and pino-pretty by @jaredwray in #243
7375
+ - mono - chore: upgrade Docker Node.js runtime image by @jaredwray in #244
7376
+ - mono - chore: pin the Compose image by @jaredwray in #245
7377
+ - Store request bins in Keyv by @jaredwray in #246
7378
+ - Store Worker request bins in MongoDB Atlas by @jaredwray in #247
7379
+
7380
+ **Full diff:** [https://github.com/jaredwray/mockhttp/compare/v2.1.0...v3.0.0](https://github.com/jaredwray/mockhttp/compare/v2.1.0...v3.0.0)
7381
+
6953
7382
  ### v2.1.0
6954
7383
  URL: https://mockhttp.org/changelog/v2-1-0
6955
7384
  Date: October 3, 2026
@@ -7615,26 +8044,3 @@ Tag: Release
7615
8044
 
7616
8045
  ## What's Changed
7617
8046
  * Adding in docker description and CORS for allowing logo.svg access
7618
-
7619
- ### v0.2.1
7620
- URL: https://mockhttp.org/changelog/v0-2-1
7621
- Date: December 13, 2024
7622
- Tag: Release
7623
-
7624
- ## What's Changed
7625
- * moving to logging on a single line by @jaredwray in [https://github.com/jaredwray/mockhttp/pull/15](https://github.com/jaredwray/mockhttp/pull/15)
7626
- * upgrading scalar to 1.25.82 by @jaredwray in [https://github.com/jaredwray/mockhttp/pull/16](https://github.com/jaredwray/mockhttp/pull/16)
7627
-
7628
-
7629
- **Full Changelog**: [https://github.com/jaredwray/mockhttp/compare/v0.2.0...v0.2.1](https://github.com/jaredwray/mockhttp/compare/v0.2.0...v0.2.1)
7630
-
7631
- ### v0.2.0
7632
- URL: https://mockhttp.org/changelog/v0-2-0
7633
- Date: December 13, 2024
7634
- Tag: Release
7635
-
7636
- ## What's Changed
7637
- * response inspection by @jaredwray in [https://github.com/jaredwray/mockhttp/pull/14](https://github.com/jaredwray/mockhttp/pull/14)
7638
-
7639
-
7640
- **Full Changelog**: [https://github.com/jaredwray/mockhttp/compare/v0.1.0...v0.2.0](https://github.com/jaredwray/mockhttp/compare/v0.1.0...v0.2.0)