@jaredwray/mockhttp 1.8.1 → 2.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.
Files changed (64) hide show
  1. package/README.md +29 -7
  2. package/dist/index.d.mts +135 -33
  3. package/dist/index.mjs +360 -342
  4. package/package.json +22 -18
  5. package/site/dist/api/index.html +17983 -0
  6. package/site/dist/api/swagger.json +5487 -0
  7. package/site/dist/changelog/index.html +767 -0
  8. package/site/dist/changelog/page/2/index.html +578 -0
  9. package/site/dist/changelog/v0-1-0/index.html +453 -0
  10. package/site/dist/changelog/v0-2-0/index.html +437 -0
  11. package/site/dist/changelog/v0-2-1/index.html +438 -0
  12. package/site/dist/changelog/v0-2-2/index.html +436 -0
  13. package/site/dist/changelog/v0-2-3/index.html +440 -0
  14. package/site/dist/changelog/v0-3-0/index.html +443 -0
  15. package/site/dist/changelog/v0-4-0/index.html +437 -0
  16. package/site/dist/changelog/v0-4-1/index.html +441 -0
  17. package/site/dist/changelog/v0-4-2/index.html +446 -0
  18. package/site/dist/changelog/v0-4-3/index.html +437 -0
  19. package/site/dist/changelog/v0-5-0/index.html +443 -0
  20. package/site/dist/changelog/v0-6-0/index.html +444 -0
  21. package/site/dist/changelog/v0-6-1/index.html +441 -0
  22. package/site/dist/changelog/v0-7-0/index.html +445 -0
  23. package/site/dist/changelog/v0-8-0/index.html +440 -0
  24. package/site/dist/changelog/v0-9-0/index.html +437 -0
  25. package/site/dist/changelog/v1-0-0/index.html +446 -0
  26. package/site/dist/changelog/v1-0-1/index.html +437 -0
  27. package/site/dist/changelog/v1-1-0/index.html +445 -0
  28. package/site/dist/changelog/v1-2-0/index.html +443 -0
  29. package/site/dist/changelog/v1-3-0/index.html +448 -0
  30. package/site/dist/changelog/v1-4-0/index.html +445 -0
  31. package/site/dist/changelog/v1-5-0/index.html +439 -0
  32. package/site/dist/changelog/v1-5-1/index.html +443 -0
  33. package/site/dist/changelog/v1-6-0/index.html +467 -0
  34. package/site/dist/changelog/v1-7-0/index.html +462 -0
  35. package/site/dist/changelog/v1-8-0/index.html +481 -0
  36. package/site/dist/changelog/v1-8-1/index.html +457 -0
  37. package/site/dist/changelog/v1-9-0/index.html +495 -0
  38. package/site/dist/changelog/v2-0-0/index.html +500 -0
  39. package/site/dist/changelog-latest.json +269 -0
  40. package/site/dist/changelog.json +399 -0
  41. package/site/dist/css/api.css +1113 -0
  42. package/site/dist/css/highlight/highlight.min.js +1433 -0
  43. package/site/dist/css/highlight/styles/base16/docula.css +123 -0
  44. package/site/dist/css/home.css +133 -0
  45. package/site/dist/css/styles.css +1227 -0
  46. package/site/dist/css/variables.css +32 -0
  47. package/site/dist/docs/bins/index.html +942 -0
  48. package/site/dist/docs/configuration/index.html +771 -0
  49. package/site/dist/docs/hosted/index.html +544 -0
  50. package/site/dist/docs/https/index.html +758 -0
  51. package/site/dist/docs/index.html +601 -0
  52. package/site/dist/docs/library/index.html +780 -0
  53. package/site/dist/docs/taps/index.html +708 -0
  54. package/site/dist/favicon.svg +14 -0
  55. package/site/dist/feed.xml +1 -0
  56. package/site/dist/index.html +601 -0
  57. package/site/dist/js/api.js +363 -0
  58. package/site/dist/js/search.js +436 -0
  59. package/site/dist/llms-full.txt +7415 -0
  60. package/site/dist/llms.txt +40 -0
  61. package/site/dist/logo.svg +21 -0
  62. package/site/dist/robots.txt +2 -0
  63. package/site/dist/search-index.json +1 -0
  64. package/site/dist/sitemap.xml +1 -0
@@ -0,0 +1,942 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <meta name="description" content="HTTP mock server and httpbin replacement for API testing. Run it at mockhttp.org, in Docker, or in Node.js.">
8
+ <link rel="stylesheet" href="/css/variables.css">
9
+ <link rel="stylesheet" href="/css/styles.css">
10
+ <link rel="stylesheet" href="/css/highlight/styles/base16/docula.css">
11
+ <link rel="icon" href="/logo.svg">
12
+ <script>
13
+ (function(){
14
+ window.__doculaThemeKey = 'docula:theme:' + ('https://mockhttp.org').replace(/^https?:\/\//, '');
15
+ var mode = localStorage.getItem(window.__doculaThemeKey) || 'light';
16
+ var resolved = mode === 'system'
17
+ ? (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark')
18
+ : mode;
19
+ if (resolved === 'light') document.documentElement.setAttribute('data-theme', 'light');
20
+ try {
21
+ var authState = JSON.parse(localStorage.getItem('docula-auth-state'));
22
+ if (authState) {
23
+ window.__doculaAuth = authState;
24
+ if (authState.loggedIn) {
25
+ document.documentElement.classList.add('docula-auth-logged-in');
26
+ }
27
+ }
28
+ } catch (e) {
29
+ // Ignore localStorage parsing errors
30
+ }
31
+ if (!window.__doculaAuth) {
32
+ window.__doculaAuth = { loggedIn: false, displayName: null };
33
+ }
34
+ })();
35
+ </script>
36
+ <title>MockHTTP - Bins</title>
37
+ <script type="application/ld+json">
38
+ {"@context":"https://schema.org","@type":"TechArticle","headline":"Bins","description":"","url":"https://mockhttp.org/docs/bins/","publisher":{"@type":"Organization","name":"MockHTTP"},"dateModified":"2026-09-18","keywords":[]}
39
+ </script>
40
+ </head>
41
+
42
+ <body>
43
+ <header class="header">
44
+ <div class="header-top">
45
+ <div class="header-top__content">
46
+ <button class="mobile-menu-toggle" id="mobile-menu-toggle" aria-label="Toggle navigation menu">
47
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>
48
+ </button>
49
+ <a href="/" class="logo-link">
50
+ <img alt="MockHTTP" class="logo__img" src="/logo.svg">
51
+ </a>
52
+ <nav class="header-bottom__nav">
53
+ <a class="header-bottom__item" href="/docs/" id="nav-docs">
54
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>
55
+ <span>Documentation</span>
56
+ </a>
57
+ <a class="header-bottom__item" href="/api" id="nav-api">
58
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/></svg>
59
+ <span>API Reference</span>
60
+ </a>
61
+ <a class="header-bottom__item" href="/changelog/" id="nav-changelog">
62
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 6a13 13 0 0 0 8.4-2.8A1 1 0 0 1 21 4v12a1 1 0 0 1-1.6.8A13 13 0 0 0 11 14H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z"/><path d="M6 14a12 12 0 0 0 2.4 7.2 2 2 0 0 0 3.2-2.4A8 8 0 0 1 10 14"/><path d="M8 6v8"/></svg>
63
+ <span>Changelog</span>
64
+ </a>
65
+ </nav>
66
+ <div class="header-actions">
67
+ <button class="search-button" id="search-button" type="button" aria-label="Search" aria-keyshortcuts="Meta+K Control+K">
68
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
69
+ <span class="search-button__text">Search</span>
70
+ <span class="search-button__shortcut">⌘K</span>
71
+ </button>
72
+ <button class="theme-button" id="theme-toggle" aria-label="Theme: system (following system)">
73
+ <svg class="theme-button__icon theme-button__icon--system" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/></svg>
74
+ <svg class="theme-button__icon theme-button__icon--sun theme-button__icon--hidden" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>
75
+ <svg class="theme-button__icon theme-button__icon--moon theme-button__icon--hidden" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"/></svg>
76
+ </button>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ </header>
81
+ <aside class="mobile-sidebar" id="mobile-sidebar">
82
+ <nav class="mobile-nav">
83
+ <a class="mobile-nav__item" href="/docs/">
84
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>
85
+ <span>Documentation</span>
86
+ </a>
87
+ <a class="mobile-nav__item" href="/api">
88
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/></svg>
89
+ <span>API Reference</span>
90
+ </a>
91
+ <a class="mobile-nav__item" href="/changelog/">
92
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 6a13 13 0 0 0 8.4-2.8A1 1 0 0 1 21 4v12a1 1 0 0 1-1.6.8A13 13 0 0 0 11 14H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z"/><path d="M6 14a12 12 0 0 0 2.4 7.2 2 2 0 0 0 3.2-2.4A8 8 0 0 1 10 14"/><path d="M8 6v8"/></svg>
93
+ <span>Changelog</span>
94
+ </a>
95
+ </nav>
96
+ <div class="mobile-sidebar__docs" id="mobile-sidebar-docs"></div>
97
+ </aside>
98
+ <div class="sidebar-backdrop" id="sidebar-backdrop"></div>
99
+ <div class="search-modal" id="search-modal" hidden>
100
+ <div class="search-modal__backdrop" data-search-close></div>
101
+ <div class="search-modal__panel" role="dialog" aria-modal="true" aria-label="Search">
102
+ <div class="search-modal__header">
103
+ <svg class="search-modal__icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
104
+ <input
105
+ type="search"
106
+ id="search-input"
107
+ class="search-modal__input"
108
+ placeholder="Search"
109
+ autocomplete="off"
110
+ autocorrect="off"
111
+ autocapitalize="off"
112
+ spellcheck="false"
113
+ aria-label="Search"
114
+ aria-controls="search-results"
115
+ aria-activedescendant=""
116
+ role="combobox"
117
+ aria-expanded="true"
118
+ />
119
+ <button class="search-modal__clear" id="search-clear" type="button" aria-label="Clear search" hidden>
120
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
121
+ </button>
122
+ </div>
123
+ <div class="search-modal__body">
124
+ <ul class="search-results" id="search-results" role="listbox" aria-label="Search results"></ul>
125
+ <div class="search-modal__empty" id="search-empty" hidden>
126
+ No results for "<span class="search-modal__empty-query" id="search-empty-query"></span>"
127
+ </div>
128
+ <div class="search-modal__initial" id="search-initial">
129
+ Type to search the documentation.
130
+ </div>
131
+ </div>
132
+ <div class="search-modal__footer">
133
+ <span class="search-modal__hint"><kbd class="search-modal__kbd">↑</kbd><kbd class="search-modal__kbd">↓</kbd> to navigate</span>
134
+ <span class="search-modal__hint"><kbd class="search-modal__kbd">↵</kbd> to select</span>
135
+ <span class="search-modal__hint"><kbd class="search-modal__kbd">esc</kbd> to close</span>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ <script>
140
+ window.__doculaSearch = { indexUrl: "/search-index.json" };
141
+ </script>
142
+ <script src="/js/search.js" defer></script>
143
+ <div class="page-dropdown" id="page-dropdown">
144
+ <button class="page-dropdown__toggle" id="page-dropdown-toggle">
145
+ <span class="page-dropdown__title">Bins</span>
146
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>
147
+ </button>
148
+ <div class="page-dropdown__list" id="page-dropdown-list">
149
+ <section class="nav-sidebar__section">
150
+ <ul class="nav-sidebar__list">
151
+ <li>
152
+ <a href="/docs/" class="nav-sidebar__item">Getting Started</a>
153
+ </li>
154
+ </ul>
155
+ </section>
156
+ <section class="nav-sidebar__section">
157
+ <ul class="nav-sidebar__list">
158
+ <li>
159
+ <a href="/docs/https/" class="nav-sidebar__item">HTTPS and HTTP/2</a>
160
+ </li>
161
+ </ul>
162
+ </section>
163
+ <section class="nav-sidebar__section">
164
+ <ul class="nav-sidebar__list">
165
+ <li>
166
+ <a href="/docs/taps/" class="nav-sidebar__item">Taps</a>
167
+ </li>
168
+ </ul>
169
+ </section>
170
+ <section class="nav-sidebar__section">
171
+ <ul class="nav-sidebar__list">
172
+ <li>
173
+ <a href="/docs/bins/" class="nav-sidebar__item">Bins</a>
174
+ </li>
175
+ </ul>
176
+ </section>
177
+ <section class="nav-sidebar__section">
178
+ <ul class="nav-sidebar__list">
179
+ <li>
180
+ <a href="/docs/configuration/" class="nav-sidebar__item">Configuration</a>
181
+ </li>
182
+ </ul>
183
+ </section>
184
+ <section class="nav-sidebar__section">
185
+ <ul class="nav-sidebar__list">
186
+ <li>
187
+ <a href="/docs/library/" class="nav-sidebar__item">Library API</a>
188
+ </li>
189
+ </ul>
190
+ </section>
191
+ <section class="nav-sidebar__section">
192
+ <ul class="nav-sidebar__list">
193
+ <li>
194
+ <a href="/docs/hosted/" class="nav-sidebar__item">Hosted Service</a>
195
+ </li>
196
+ </ul>
197
+ </section>
198
+ </div>
199
+ </div>
200
+ <div class="content">
201
+ <aside class="nav-sidebar" id="nav-sidebar">
202
+ <section class="nav-sidebar__section">
203
+ <ul class="nav-sidebar__list">
204
+ <li>
205
+ <a href="/docs/" class="nav-sidebar__item">Getting Started</a>
206
+ </li>
207
+ </ul>
208
+ </section>
209
+ <section class="nav-sidebar__section">
210
+ <ul class="nav-sidebar__list">
211
+ <li>
212
+ <a href="/docs/https/" class="nav-sidebar__item">HTTPS and HTTP/2</a>
213
+ </li>
214
+ </ul>
215
+ </section>
216
+ <section class="nav-sidebar__section">
217
+ <ul class="nav-sidebar__list">
218
+ <li>
219
+ <a href="/docs/taps/" class="nav-sidebar__item">Taps</a>
220
+ </li>
221
+ </ul>
222
+ </section>
223
+ <section class="nav-sidebar__section">
224
+ <ul class="nav-sidebar__list">
225
+ <li>
226
+ <a href="/docs/bins/" class="nav-sidebar__item">Bins</a>
227
+ </li>
228
+ </ul>
229
+ </section>
230
+ <section class="nav-sidebar__section">
231
+ <ul class="nav-sidebar__list">
232
+ <li>
233
+ <a href="/docs/configuration/" class="nav-sidebar__item">Configuration</a>
234
+ </li>
235
+ </ul>
236
+ </section>
237
+ <section class="nav-sidebar__section">
238
+ <ul class="nav-sidebar__list">
239
+ <li>
240
+ <a href="/docs/library/" class="nav-sidebar__item">Library API</a>
241
+ </li>
242
+ </ul>
243
+ </section>
244
+ <section class="nav-sidebar__section">
245
+ <ul class="nav-sidebar__list">
246
+ <li>
247
+ <a href="/docs/hosted/" class="nav-sidebar__item">Hosted Service</a>
248
+ </li>
249
+ </ul>
250
+ </section>
251
+ </aside>
252
+ <article class="article">
253
+ <header class="article__header">
254
+ <h1 class="article__title">Bins</h1>
255
+ </header>
256
+ <div class="article__body">
257
+ <main class="article__main">
258
+ <p>Bins are ephemeral URL endpoints that <strong>capture incoming HTTP requests</strong> so you
259
+ can inspect them later. They're the inverse of Taps: instead of injecting a
260
+ response, they record everything they receive. Useful for:</p>
261
+ <ul>
262
+ <li><strong>Debugging webhooks</strong> — point Stripe / GitHub / Slack at a bin URL and see
263
+ exactly what they send</li>
264
+ <li><strong>Exercising client SDKs</strong> — verify your SDK sends the request shape you expect</li>
265
+ <li><strong>Recording fixtures</strong> — capture real traffic to replay in tests later</li>
266
+ </ul>
267
+ <p>There are two URL prefixes:</p>
268
+ <ul>
269
+ <li><strong><code>/bins</code></strong> — JSON management API: create, list, inspect, delete bins</li>
270
+ <li><strong><code>/b/:id</code></strong> — the capture URL. Any HTTP method, any sub-path, any body sent
271
+ here is stored against bin <code>:id</code></li>
272
+ </ul>
273
+ <h2 id="table-of-contents">Table of Contents</h2>
274
+ <ul>
275
+ <li><a href="#quick-start">Quick Start</a></li>
276
+ <li><a href="#endpoints">Endpoints</a></li>
277
+ <li><a href="#end-to-end-webhook-debugging-example">End-to-End Webhook Debugging Example</a></li>
278
+ <li><a href="#captured-request-shape">Captured Request Shape</a></li>
279
+ <li><a href="#body-encoding-examples">Body Encoding Examples</a>
280
+ <ul>
281
+ <li><a href="#text-content-utf-8">Text Content (UTF-8)</a></li>
282
+ <li><a href="#binary-content-base64">Binary Content (Base64)</a></li>
283
+ <li><a href="#truncation">Truncation</a></li>
284
+ </ul>
285
+ </li>
286
+ <li><a href="#fifo-eviction-example">FIFO Eviction Example</a></li>
287
+ <li><a href="#programmatic-access">Programmatic Access</a>
288
+ <ul>
289
+ <li><a href="#using-bins-in-a-vitest-test">Using Bins in a Vitest Test</a></li>
290
+ </ul>
291
+ </li>
292
+ <li><a href="#configuration">Configuration</a>
293
+ <ul>
294
+ <li><a href="#defaults">Defaults</a></li>
295
+ <li><a href="#pluggable-storage">Pluggable Storage</a></li>
296
+ </ul>
297
+ </li>
298
+ </ul>
299
+ <h2 id="quick-start">Quick Start</h2>
300
+ <pre><code class="hljs language-bash"><span class="hljs-comment"># 1. create a bin</span>
301
+ curl -s -X POST http://localhost:3000/bins
302
+ <span class="hljs-comment"># → { "id": "abc123def456", "url": "http://localhost:3000/b/abc123def456",</span>
303
+ <span class="hljs-comment"># "createdAt": "2026-05-18T12:00:00.000Z",</span>
304
+ <span class="hljs-comment"># "expiresAt": "2026-05-19T12:00:00.000Z",</span>
305
+ <span class="hljs-comment"># "requestCount": 0 }</span>
306
+
307
+ <span class="hljs-comment"># 2. send anything to the capture URL</span>
308
+ curl -X POST <span class="hljs-string">"http://localhost:3000/b/abc123def456/webhook?source=stripe"</span> \
309
+ -H <span class="hljs-string">'content-type: application/json'</span> \
310
+ -d <span class="hljs-string">'{"event":"payment.succeeded"}'</span>
311
+ <span class="hljs-comment"># → { "ok": true, "binId": "abc123def456", "requestId": "f9c2e1a40b3d" }</span>
312
+
313
+ <span class="hljs-comment"># 3. list captured requests (newest first)</span>
314
+ curl http://localhost:3000/bins/abc123def456/requests
315
+ </code></pre>
316
+ <h2 id="endpoints">Endpoints</h2>
317
+ <table>
318
+ <thead>
319
+ <tr>
320
+ <th>Method</th>
321
+ <th>Path</th>
322
+ <th>Description</th>
323
+ </tr>
324
+ </thead>
325
+ <tbody>
326
+ <tr>
327
+ <td>POST</td>
328
+ <td><code>/bins</code></td>
329
+ <td>Create a new bin</td>
330
+ </tr>
331
+ <tr>
332
+ <td>GET</td>
333
+ <td><code>/bins</code></td>
334
+ <td>List all active bins</td>
335
+ </tr>
336
+ <tr>
337
+ <td>GET</td>
338
+ <td><code>/bins/:id</code></td>
339
+ <td>Get bin metadata</td>
340
+ </tr>
341
+ <tr>
342
+ <td>GET</td>
343
+ <td><code>/bins/:id/requests</code></td>
344
+ <td>List captured requests (newest first)</td>
345
+ </tr>
346
+ <tr>
347
+ <td>GET</td>
348
+ <td><code>/bins/:id/requests/:reqId</code></td>
349
+ <td>Get a single captured request</td>
350
+ </tr>
351
+ <tr>
352
+ <td>DELETE</td>
353
+ <td><code>/bins/:id/requests</code></td>
354
+ <td>Clear all captured requests in a bin</td>
355
+ </tr>
356
+ <tr>
357
+ <td>DELETE</td>
358
+ <td><code>/bins/:id</code></td>
359
+ <td>Delete a bin</td>
360
+ </tr>
361
+ <tr>
362
+ <td>ANY</td>
363
+ <td><code>/b/:id</code> and <code>/b/:id/*</code></td>
364
+ <td>Capture requests sent to a bin</td>
365
+ </tr>
366
+ </tbody>
367
+ </table>
368
+ <h2 id="end-to-end-webhook-debugging-example">End-to-End Webhook Debugging Example</h2>
369
+ <p>A complete walkthrough using curl:</p>
370
+ <pre><code class="hljs language-bash">BASE=http://localhost:3000
371
+
372
+ <span class="hljs-comment"># Create the bin and grab its id</span>
373
+ ID=$(curl -s -X POST <span class="hljs-variable">$BASE</span>/bins | jq -r .<span class="hljs-built_in">id</span>)
374
+ <span class="hljs-built_in">echo</span> <span class="hljs-string">"Bin URL: <span class="hljs-variable">$BASE</span>/b/<span class="hljs-variable">$ID</span>"</span>
375
+
376
+ <span class="hljs-comment"># Simulate a Stripe webhook</span>
377
+ curl -s -X POST <span class="hljs-string">"<span class="hljs-variable">$BASE</span>/b/<span class="hljs-variable">$ID</span>/stripe/events?secret=whsec_test"</span> \
378
+ -H <span class="hljs-string">'content-type: application/json'</span> \
379
+ -H <span class="hljs-string">'stripe-signature: t=1700000000,v1=abc123'</span> \
380
+ -d <span class="hljs-string">'{"id":"evt_1","type":"payment_intent.succeeded","data":{"object":{"amount":2000}}}'</span>
381
+
382
+ <span class="hljs-comment"># Simulate a GitHub webhook</span>
383
+ curl -s -X POST <span class="hljs-string">"<span class="hljs-variable">$BASE</span>/b/<span class="hljs-variable">$ID</span>/github"</span> \
384
+ -H <span class="hljs-string">'content-type: application/json'</span> \
385
+ -H <span class="hljs-string">'x-github-event: pull_request'</span> \
386
+ -d <span class="hljs-string">'{"action":"opened","number":42}'</span>
387
+
388
+ <span class="hljs-comment"># Inspect everything that arrived</span>
389
+ curl -s <span class="hljs-string">"<span class="hljs-variable">$BASE</span>/bins/<span class="hljs-variable">$ID</span>/requests"</span> | jq
390
+
391
+ <span class="hljs-comment"># Drill into the most recent one</span>
392
+ RID=$(curl -s <span class="hljs-string">"<span class="hljs-variable">$BASE</span>/bins/<span class="hljs-variable">$ID</span>/requests"</span> | jq -r <span class="hljs-string">'.requests[0].id'</span>)
393
+ curl -s <span class="hljs-string">"<span class="hljs-variable">$BASE</span>/bins/<span class="hljs-variable">$ID</span>/requests/<span class="hljs-variable">$RID</span>"</span> | jq
394
+ <span class="hljs-comment"># {</span>
395
+ <span class="hljs-comment"># "id": "f9c2e1a40b3d",</span>
396
+ <span class="hljs-comment"># "binId": "abc123def456",</span>
397
+ <span class="hljs-comment"># "method": "POST",</span>
398
+ <span class="hljs-comment"># "url": "/github",</span>
399
+ <span class="hljs-comment"># "path": "/github",</span>
400
+ <span class="hljs-comment"># "query": {},</span>
401
+ <span class="hljs-comment"># "headers": {</span>
402
+ <span class="hljs-comment"># "host": "localhost:3000",</span>
403
+ <span class="hljs-comment"># "content-type": "application/json",</span>
404
+ <span class="hljs-comment"># "x-github-event": "pull_request"</span>
405
+ <span class="hljs-comment"># },</span>
406
+ <span class="hljs-comment"># "remoteAddress": "127.0.0.1",</span>
407
+ <span class="hljs-comment"># "contentType": "application/json",</span>
408
+ <span class="hljs-comment"># "bodySize": 32,</span>
409
+ <span class="hljs-comment"># "body": "{\"action\":\"opened\",\"number\":42}",</span>
410
+ <span class="hljs-comment"># "bodyEncoding": "utf8",</span>
411
+ <span class="hljs-comment"># "truncated": false,</span>
412
+ <span class="hljs-comment"># "capturedAt": "2026-05-18T12:00:01.234Z"</span>
413
+ <span class="hljs-comment"># }</span>
414
+
415
+ <span class="hljs-comment"># Clean up</span>
416
+ curl -s -X DELETE <span class="hljs-string">"<span class="hljs-variable">$BASE</span>/bins/<span class="hljs-variable">$ID</span>/requests"</span> <span class="hljs-comment"># clear captures, keep the bin</span>
417
+ curl -s -X DELETE <span class="hljs-string">"<span class="hljs-variable">$BASE</span>/bins/<span class="hljs-variable">$ID</span>"</span> <span class="hljs-comment"># delete the bin entirely</span>
418
+ </code></pre>
419
+ <h2 id="captured-request-shape">Captured Request Shape</h2>
420
+ <pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
421
+ <span class="hljs-attr">"id"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"f9c2e1a40b3d"</span><span class="hljs-punctuation">,</span>
422
+ <span class="hljs-attr">"binId"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"abc123def456"</span><span class="hljs-punctuation">,</span>
423
+ <span class="hljs-attr">"method"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"POST"</span><span class="hljs-punctuation">,</span>
424
+ <span class="hljs-attr">"url"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"/webhook?source=stripe"</span><span class="hljs-punctuation">,</span>
425
+ <span class="hljs-attr">"path"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"/webhook"</span><span class="hljs-punctuation">,</span>
426
+ <span class="hljs-attr">"query"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span> <span class="hljs-attr">"source"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"stripe"</span> <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
427
+ <span class="hljs-attr">"headers"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span> <span class="hljs-attr">"content-type"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"application/json"</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">"..."</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"..."</span> <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
428
+ <span class="hljs-attr">"remoteAddress"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"127.0.0.1"</span><span class="hljs-punctuation">,</span>
429
+ <span class="hljs-attr">"contentType"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"application/json"</span><span class="hljs-punctuation">,</span>
430
+ <span class="hljs-attr">"bodySize"</span><span class="hljs-punctuation">:</span> <span class="hljs-number">32</span><span class="hljs-punctuation">,</span>
431
+ <span class="hljs-attr">"body"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"{\"event\":\"payment.succeeded\"}"</span><span class="hljs-punctuation">,</span>
432
+ <span class="hljs-attr">"bodyEncoding"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"utf8"</span><span class="hljs-punctuation">,</span>
433
+ <span class="hljs-attr">"truncated"</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">false</span></span><span class="hljs-punctuation">,</span>
434
+ <span class="hljs-attr">"capturedAt"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"2026-05-18T12:00:00.000Z"</span>
435
+ <span class="hljs-punctuation">}</span>
436
+ </code></pre>
437
+ <ul>
438
+ <li><strong><code>url</code></strong> is the request URL relative to the bin (everything after <code>/b/:id</code>).</li>
439
+ <li><strong><code>path</code></strong> is the sub-path only, without the query string.</li>
440
+ <li><strong><code>bodyEncoding</code></strong> is <code>"utf8"</code> for text content types (text/*, application/json,
441
+ application/xml, application/x-www-form-urlencoded, application/javascript),
442
+ <code>"base64"</code> for everything else (binary payloads), or <code>"none"</code> when no body
443
+ was sent.</li>
444
+ <li><strong><code>truncated</code></strong> is <code>true</code> when the body exceeded <code>maxBodySize</code> and was cut
445
+ off. <code>bodySize</code> reflects the original length.</li>
446
+ </ul>
447
+ <h2 id="body-encoding-examples">Body Encoding Examples</h2>
448
+ <h3 id="text-content-utf-8">Text Content (UTF-8)</h3>
449
+ <p>JSON, XML, form-encoded, plain text, and JavaScript are stored as UTF-8 strings
450
+ so you can read them directly:</p>
451
+ <pre><code class="hljs language-bash">curl -X POST <span class="hljs-string">"http://localhost:3000/b/<span class="hljs-variable">$ID</span>"</span> \
452
+ -H <span class="hljs-string">'content-type: application/json'</span> \
453
+ -d <span class="hljs-string">'{"hello":"world"}'</span>
454
+
455
+ curl -s <span class="hljs-string">"http://localhost:3000/bins/<span class="hljs-variable">$ID</span>/requests"</span> | jq <span class="hljs-string">'.requests[0] | {bodyEncoding, body}'</span>
456
+ <span class="hljs-comment"># {</span>
457
+ <span class="hljs-comment"># "bodyEncoding": "utf8",</span>
458
+ <span class="hljs-comment"># "body": "{\"hello\":\"world\"}"</span>
459
+ <span class="hljs-comment"># }</span>
460
+ </code></pre>
461
+ <h3 id="binary-content-base64">Binary Content (Base64)</h3>
462
+ <p>Any non-text content type is base64-encoded, preserving the bytes exactly:</p>
463
+ <pre><code class="hljs language-bash"><span class="hljs-comment"># Upload a PNG to the bin</span>
464
+ curl -X POST <span class="hljs-string">"http://localhost:3000/b/<span class="hljs-variable">$ID</span>/upload"</span> \
465
+ -H <span class="hljs-string">'content-type: image/png'</span> \
466
+ --data-binary @logo.png
467
+
468
+ curl -s <span class="hljs-string">"http://localhost:3000/bins/<span class="hljs-variable">$ID</span>/requests"</span> | jq <span class="hljs-string">'.requests[0] | {bodyEncoding, bodySize}'</span>
469
+ <span class="hljs-comment"># {</span>
470
+ <span class="hljs-comment"># "bodyEncoding": "base64",</span>
471
+ <span class="hljs-comment"># "bodySize": 14823</span>
472
+ <span class="hljs-comment"># }</span>
473
+
474
+ <span class="hljs-comment"># Decode the body back to bytes</span>
475
+ curl -s <span class="hljs-string">"http://localhost:3000/bins/<span class="hljs-variable">$ID</span>/requests"</span> \
476
+ | jq -r <span class="hljs-string">'.requests[0].body'</span> | <span class="hljs-built_in">base64</span> -d > recovered.png
477
+ </code></pre>
478
+ <h3 id="truncation">Truncation</h3>
479
+ <p>Bodies larger than <code>maxBodySize</code> (1 MiB by default) are cut off and flagged:</p>
480
+ <pre><code class="hljs language-javascript"><span class="hljs-comment">// In test setup</span>
481
+ <span class="hljs-keyword">import</span> { <span class="hljs-title class_">MockHttp</span>, <span class="hljs-title class_">BinManager</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@jaredwray/mockhttp'</span>;
482
+ <span class="hljs-keyword">const</span> mock = <span class="hljs-keyword">new</span> <span class="hljs-title class_">MockHttp</span>();
483
+ mock.<span class="hljs-property">bins</span> = <span class="hljs-keyword">new</span> <span class="hljs-title class_">BinManager</span>({ <span class="hljs-attr">maxBodySize</span>: <span class="hljs-number">1024</span> }); <span class="hljs-comment">// 1 KiB cap</span>
484
+ <span class="hljs-keyword">await</span> mock.<span class="hljs-title function_">start</span>();
485
+ </code></pre>
486
+ <pre><code class="hljs language-bash"><span class="hljs-comment"># Send 5000 bytes to a bin with a 1 KiB cap</span>
487
+ <span class="hljs-built_in">head</span> -c 5000 /dev/urandom | curl -X POST <span class="hljs-string">"http://localhost:3000/b/<span class="hljs-variable">$ID</span>"</span> \
488
+ -H <span class="hljs-string">'content-type: application/octet-stream'</span> \
489
+ --data-binary @-
490
+
491
+ curl -s <span class="hljs-string">"http://localhost:3000/bins/<span class="hljs-variable">$ID</span>/requests"</span> | jq <span class="hljs-string">'.requests[0] | {bodySize, truncated, body_len: (.body | length)}'</span>
492
+ <span class="hljs-comment"># {</span>
493
+ <span class="hljs-comment"># "bodySize": 5000, ← original size</span>
494
+ <span class="hljs-comment"># "truncated": true,</span>
495
+ <span class="hljs-comment"># "body_len": 1368 ← base64 of the first 1024 bytes</span>
496
+ <span class="hljs-comment"># }</span>
497
+ </code></pre>
498
+ <h2 id="fifo-eviction-example">FIFO Eviction Example</h2>
499
+ <p>Once a bin reaches <code>maxRequestsPerBin</code> captures (100 by default), the oldest
500
+ ones are dropped:</p>
501
+ <pre><code class="hljs language-bash"><span class="hljs-comment"># Send 105 requests</span>
502
+ <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> $(<span class="hljs-built_in">seq</span> 1 105); <span class="hljs-keyword">do</span>
503
+ curl -s -X POST <span class="hljs-string">"http://localhost:3000/b/<span class="hljs-variable">$ID</span>/event/<span class="hljs-variable">$i</span>"</span> > /dev/null
504
+ <span class="hljs-keyword">done</span>
505
+
506
+ <span class="hljs-comment"># The bin holds the most recent 100; the first 5 are gone</span>
507
+ curl -s <span class="hljs-string">"http://localhost:3000/bins/<span class="hljs-variable">$ID</span>/requests"</span> | jq <span class="hljs-string">'.requests | length'</span>
508
+ <span class="hljs-comment"># 100</span>
509
+
510
+ <span class="hljs-comment"># Newest is at the top</span>
511
+ curl -s <span class="hljs-string">"http://localhost:3000/bins/<span class="hljs-variable">$ID</span>/requests"</span> | jq <span class="hljs-string">'.requests[0].path'</span>
512
+ <span class="hljs-comment"># "/event/105"</span>
513
+ </code></pre>
514
+ <h2 id="programmatic-access">Programmatic Access</h2>
515
+ <p>The bin manager is exposed on the <code>MockHttp</code> instance as <code>mock.bins</code>. This
516
+ makes it easy to drive bins from a test suite without going through HTTP:</p>
517
+ <pre><code class="hljs language-javascript"><span class="hljs-keyword">import</span> { <span class="hljs-title class_">MockHttp</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@jaredwray/mockhttp'</span>;
518
+
519
+ <span class="hljs-keyword">const</span> mock = <span class="hljs-keyword">new</span> <span class="hljs-title class_">MockHttp</span>({ <span class="hljs-attr">logging</span>: <span class="hljs-literal">false</span> });
520
+ <span class="hljs-keyword">await</span> mock.<span class="hljs-title function_">start</span>();
521
+
522
+ <span class="hljs-comment">// Create a bin</span>
523
+ <span class="hljs-keyword">const</span> bin = mock.<span class="hljs-property">bins</span>.<span class="hljs-title function_">createBin</span>();
524
+ <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">`Webhook target: http://localhost:<span class="hljs-subst">${mock.port}</span>/b/<span class="hljs-subst">${bin.id}</span>`</span>);
525
+
526
+ <span class="hljs-comment">// ... point your code-under-test at that URL ...</span>
527
+
528
+ <span class="hljs-comment">// Read captured requests (newest first)</span>
529
+ <span class="hljs-keyword">const</span> requests = mock.<span class="hljs-property">bins</span>.<span class="hljs-title function_">getRequests</span>(bin.<span class="hljs-property">id</span>);
530
+ <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">`Captured <span class="hljs-subst">${requests.length}</span> requests`</span>);
531
+
532
+ <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> req <span class="hljs-keyword">of</span> requests) {
533
+ <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">`<span class="hljs-subst">${req.method}</span> <span class="hljs-subst">${req.path}</span> (<span class="hljs-subst">${req.bodySize}</span> bytes)`</span>);
534
+ }
535
+
536
+ <span class="hljs-comment">// Clean up</span>
537
+ mock.<span class="hljs-property">bins</span>.<span class="hljs-title function_">deleteBin</span>(bin.<span class="hljs-property">id</span>);
538
+ <span class="hljs-keyword">await</span> mock.<span class="hljs-title function_">close</span>(); <span class="hljs-comment">// also stops the bin cleanup timer</span>
539
+ </code></pre>
540
+ <h3 id="using-bins-in-a-vitest-test">Using Bins in a Vitest Test</h3>
541
+ <pre><code class="hljs language-typescript"><span class="hljs-keyword">import</span> { afterAll, beforeAll, expect, test } <span class="hljs-keyword">from</span> <span class="hljs-string">'vitest'</span>;
542
+ <span class="hljs-keyword">import</span> { <span class="hljs-title class_">MockHttp</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@jaredwray/mockhttp'</span>;
543
+
544
+ <span class="hljs-keyword">let</span> <span class="hljs-attr">mock</span>: <span class="hljs-title class_">MockHttp</span>;
545
+
546
+ <span class="hljs-title function_">beforeAll</span>(<span class="hljs-title function_">async</span> () => {
547
+ mock = <span class="hljs-keyword">new</span> <span class="hljs-title class_">MockHttp</span>({ <span class="hljs-attr">logging</span>: <span class="hljs-literal">false</span> });
548
+ <span class="hljs-keyword">await</span> mock.<span class="hljs-title function_">start</span>();
549
+ });
550
+
551
+ <span class="hljs-title function_">afterAll</span>(<span class="hljs-title function_">async</span> () => {
552
+ <span class="hljs-keyword">await</span> mock.<span class="hljs-title function_">close</span>();
553
+ });
554
+
555
+ <span class="hljs-title function_">test</span>(<span class="hljs-string">'my SDK sends the right webhook payload'</span>, <span class="hljs-title function_">async</span> () => {
556
+ <span class="hljs-keyword">const</span> bin = mock.<span class="hljs-property">bins</span>.<span class="hljs-title function_">createBin</span>();
557
+ <span class="hljs-keyword">const</span> webhookUrl = <span class="hljs-string">`http://localhost:<span class="hljs-subst">${mock.port}</span>/b/<span class="hljs-subst">${bin.id}</span>`</span>;
558
+
559
+ <span class="hljs-comment">// Drive your SDK at the bin</span>
560
+ <span class="hljs-keyword">await</span> mySdk.<span class="hljs-title function_">notify</span>(webhookUrl, { <span class="hljs-attr">event</span>: <span class="hljs-string">'user.created'</span>, <span class="hljs-attr">id</span>: <span class="hljs-number">42</span> });
561
+
562
+ <span class="hljs-comment">// Assert on what actually arrived</span>
563
+ <span class="hljs-keyword">const</span> [captured] = mock.<span class="hljs-property">bins</span>.<span class="hljs-title function_">getRequests</span>(bin.<span class="hljs-property">id</span>);
564
+ <span class="hljs-title function_">expect</span>(captured.<span class="hljs-property">method</span>).<span class="hljs-title function_">toBe</span>(<span class="hljs-string">'POST'</span>);
565
+ <span class="hljs-title function_">expect</span>(captured.<span class="hljs-property">contentType</span>).<span class="hljs-title function_">toBe</span>(<span class="hljs-string">'application/json'</span>);
566
+ <span class="hljs-title function_">expect</span>(<span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">parse</span>(captured.<span class="hljs-property">body</span>!)).<span class="hljs-title function_">toEqual</span>({
567
+ <span class="hljs-attr">event</span>: <span class="hljs-string">'user.created'</span>,
568
+ <span class="hljs-attr">id</span>: <span class="hljs-number">42</span>,
569
+ });
570
+ <span class="hljs-title function_">expect</span>(captured.<span class="hljs-property">headers</span>[<span class="hljs-string">'x-signature'</span>]).<span class="hljs-title function_">toBeDefined</span>();
571
+ });
572
+ </code></pre>
573
+ <h2 id="configuration">Configuration</h2>
574
+ <p>Bins are enabled by default. To disable the routes entirely:</p>
575
+ <pre><code class="hljs language-javascript"><span class="hljs-keyword">const</span> mock = <span class="hljs-keyword">new</span> <span class="hljs-title class_">MockHttp</span>({
576
+ <span class="hljs-attr">httpBin</span>: { <span class="hljs-attr">bins</span>: <span class="hljs-literal">false</span> },
577
+ });
578
+ </code></pre>
579
+ <p>To tune limits, replace the default <code>BinManager</code> before starting:</p>
580
+ <pre><code class="hljs language-javascript"><span class="hljs-keyword">import</span> { <span class="hljs-title class_">MockHttp</span>, <span class="hljs-title class_">BinManager</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@jaredwray/mockhttp'</span>;
581
+
582
+ <span class="hljs-keyword">const</span> mock = <span class="hljs-keyword">new</span> <span class="hljs-title class_">MockHttp</span>();
583
+ mock.<span class="hljs-property">bins</span> = <span class="hljs-keyword">new</span> <span class="hljs-title class_">BinManager</span>({
584
+ <span class="hljs-attr">defaultTtlMs</span>: <span class="hljs-number">60</span> * <span class="hljs-number">60</span> * <span class="hljs-number">1000</span>, <span class="hljs-comment">// 1 hour (default: 24h)</span>
585
+ <span class="hljs-attr">maxRequestsPerBin</span>: <span class="hljs-number">500</span>, <span class="hljs-comment">// (default: 100)</span>
586
+ <span class="hljs-attr">maxBodySize</span>: <span class="hljs-number">5</span> * <span class="hljs-number">1024</span> * <span class="hljs-number">1024</span>, <span class="hljs-comment">// 5 MiB (default: 1 MiB)</span>
587
+ <span class="hljs-attr">idLength</span>: <span class="hljs-number">16</span>, <span class="hljs-comment">// (default: 12)</span>
588
+ <span class="hljs-attr">cleanupIntervalMs</span>: <span class="hljs-number">30</span> * <span class="hljs-number">1000</span>, <span class="hljs-comment">// sweep every 30s (default: 60s)</span>
589
+ });
590
+
591
+ <span class="hljs-keyword">await</span> mock.<span class="hljs-title function_">start</span>();
592
+ </code></pre>
593
+ <h3 id="defaults">Defaults</h3>
594
+ <table>
595
+ <thead>
596
+ <tr>
597
+ <th>Option</th>
598
+ <th>Default</th>
599
+ <th>Description</th>
600
+ </tr>
601
+ </thead>
602
+ <tbody>
603
+ <tr>
604
+ <td><code>defaultTtlMs</code></td>
605
+ <td><code>86400000</code> (24h)</td>
606
+ <td>Bin lifetime. Expired bins return 404 and are lazily removed.</td>
607
+ </tr>
608
+ <tr>
609
+ <td><code>maxRequestsPerBin</code></td>
610
+ <td><code>100</code></td>
611
+ <td>When exceeded, oldest captures are dropped (FIFO).</td>
612
+ </tr>
613
+ <tr>
614
+ <td><code>maxBodySize</code></td>
615
+ <td><code>1048576</code> (1 MiB)</td>
616
+ <td>Larger bodies are truncated; <code>truncated: true</code> is set on the capture.</td>
617
+ </tr>
618
+ <tr>
619
+ <td><code>idLength</code></td>
620
+ <td><code>12</code></td>
621
+ <td>Length of generated bin and request ids.</td>
622
+ </tr>
623
+ <tr>
624
+ <td><code>cleanupIntervalMs</code></td>
625
+ <td><code>60000</code> (1 min)</td>
626
+ <td>How often expired bins are swept. The timer is <code>unref()</code>'d so it never keeps the process alive.</td>
627
+ </tr>
628
+ </tbody>
629
+ </table>
630
+ <h3 id="pluggable-storage">Pluggable Storage</h3>
631
+ <p><code>BinManager</code> accepts a <code>store: BinStore</code> so you can plug in alternative
632
+ backends (Redis, SQLite, etc.) without changing the rest of the codebase. The
633
+ default <code>InMemoryBinStore</code> keeps state in process memory.</p>
634
+ <pre><code class="hljs language-typescript"><span class="hljs-keyword">import</span> {
635
+ <span class="hljs-title class_">BinManager</span>,
636
+ <span class="hljs-title class_">InMemoryBinStore</span>,
637
+ <span class="hljs-keyword">type</span> <span class="hljs-title class_">Bin</span>,
638
+ <span class="hljs-keyword">type</span> <span class="hljs-title class_">BinStore</span>,
639
+ <span class="hljs-keyword">type</span> <span class="hljs-title class_">CapturedRequest</span>,
640
+ } <span class="hljs-keyword">from</span> <span class="hljs-string">'@jaredwray/mockhttp'</span>;
641
+
642
+ <span class="hljs-keyword">class</span> <span class="hljs-title class_">RedisBinStore</span> <span class="hljs-keyword">implements</span> <span class="hljs-title class_">BinStore</span> {
643
+ <span class="hljs-title function_">createBin</span>(<span class="hljs-attr">bin</span>: <span class="hljs-title class_">Bin</span>): <span class="hljs-built_in">void</span> { <span class="hljs-comment">/* SET bin:${bin.id} ... */</span> }
644
+ <span class="hljs-title function_">getBin</span>(<span class="hljs-attr">id</span>: <span class="hljs-built_in">string</span>): <span class="hljs-title class_">Bin</span> | <span class="hljs-literal">undefined</span> { <span class="hljs-comment">/* GET ... */</span> }
645
+ <span class="hljs-title function_">listBins</span>(): <span class="hljs-title class_">Bin</span>[] { <span class="hljs-comment">/* SCAN ... */</span> }
646
+ <span class="hljs-title function_">deleteBin</span>(<span class="hljs-attr">id</span>: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">boolean</span> { <span class="hljs-comment">/* DEL ... */</span> }
647
+ <span class="hljs-title function_">addRequest</span>(<span class="hljs-attr">binId</span>: <span class="hljs-built_in">string</span>, <span class="hljs-attr">req</span>: <span class="hljs-title class_">CapturedRequest</span>, <span class="hljs-attr">max</span>: <span class="hljs-built_in">number</span>): <span class="hljs-built_in">void</span> {
648
+ <span class="hljs-comment">/* LPUSH bin:${binId}:requests + LTRIM to max */</span>
649
+ }
650
+ <span class="hljs-title function_">getRequests</span>(<span class="hljs-attr">binId</span>: <span class="hljs-built_in">string</span>): <span class="hljs-title class_">CapturedRequest</span>[] { <span class="hljs-comment">/* LRANGE ... */</span> }
651
+ <span class="hljs-title function_">getRequest</span>(<span class="hljs-attr">binId</span>: <span class="hljs-built_in">string</span>, <span class="hljs-attr">reqId</span>: <span class="hljs-built_in">string</span>): <span class="hljs-title class_">CapturedRequest</span> | <span class="hljs-literal">undefined</span> { <span class="hljs-comment">/* ... */</span> }
652
+ <span class="hljs-title function_">clearRequests</span>(<span class="hljs-attr">binId</span>: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">void</span> { <span class="hljs-comment">/* DEL bin:${binId}:requests */</span> }
653
+ <span class="hljs-title function_">cleanupExpired</span>(<span class="hljs-attr">now</span>: <span class="hljs-built_in">number</span>): <span class="hljs-built_in">string</span>[] { <span class="hljs-comment">/* scan + delete */</span> <span class="hljs-keyword">return</span> []; }
654
+ }
655
+
656
+ mock.<span class="hljs-property">bins</span> = <span class="hljs-keyword">new</span> <span class="hljs-title class_">BinManager</span>({ <span class="hljs-attr">store</span>: <span class="hljs-keyword">new</span> <span class="hljs-title class_">RedisBinStore</span>() });
657
+ </code></pre>
658
+ </main>
659
+ <aside class="content-aside">
660
+ <h2 class="content-aside__title">TABLE OF CONTENTS</h2>
661
+ <ul class="content-aside__list" id="toc-list">
662
+ </ul>
663
+ </aside>
664
+ </div>
665
+ <a class="article__edit-link" href="https://github.com/jaredwray/mockhttp/edit/main/site/docs/bins.md" target="_blank" rel="noopener noreferrer">
666
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
667
+ Edit this page
668
+ </a>
669
+ </article>
670
+ </div>
671
+ <footer>
672
+ <a href="/llms.txt" class="footer__link">llms.txt</a>
673
+ <a href="/feed.xml" class="footer__link" aria-label="RSS Feed">
674
+ <svg class="footer__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><circle cx="6.18" cy="17.82" r="2.18"/><path d="M4 4.44v2.83c7.03 0 12.73 5.7 12.73 12.73h2.83c0-8.59-6.97-15.56-15.56-15.56zm0 5.66v2.83c3.9 0 7.07 3.17 7.07 7.07h2.83c0-5.47-4.43-9.9-9.9-9.9z"/></svg>
675
+ </a>
676
+
677
+ <a href="https://docula.org/" target="_blank" rel="noopener noreferrer" class="footer__link" aria-label="Docula">
678
+ <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>
679
+ </a>
680
+ <span class="footer__updated">updated: 2026-09-18</span>
681
+ </footer>
682
+ <script src="/css/highlight/highlight.min.js"></script>
683
+ <script>
684
+ document.addEventListener('DOMContentLoaded', () => {
685
+ document.querySelectorAll('pre code').forEach(el => { el.textContent = el.textContent.trimEnd(); });
686
+ hljs.highlightAll();
687
+ var kbd = document.querySelector('.search-button__shortcut');
688
+ if (kbd) kbd.textContent = navigator.platform.indexOf('Mac') > -1 ? '⌘K' : 'Ctrl K';
689
+
690
+ // Copy code buttons
691
+ const copyIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
692
+ const checkIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
693
+ document.querySelectorAll('pre').forEach(function(pre) {
694
+ const btn = document.createElement('button');
695
+ btn.className = 'copy-code-btn';
696
+ btn.innerHTML = copyIcon;
697
+ btn.setAttribute('aria-label', 'Copy code');
698
+ btn.addEventListener('click', function() {
699
+ const code = pre.querySelector('code');
700
+ const text = code ? code.textContent : pre.textContent;
701
+ navigator.clipboard.writeText(text || '').then(function() {
702
+ btn.innerHTML = checkIcon;
703
+ setTimeout(function() { btn.innerHTML = copyIcon; }, 2000);
704
+ });
705
+ });
706
+ pre.appendChild(btn);
707
+ });
708
+ });
709
+ </script>
710
+ <script>
711
+ (function() {
712
+ var themeKey = window.__doculaThemeKey;
713
+ function getMode() {
714
+ return localStorage.getItem(themeKey) || 'light';
715
+ }
716
+
717
+ function resolveTheme(mode) {
718
+ if (mode === 'system') {
719
+ return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
720
+ }
721
+ return mode;
722
+ }
723
+
724
+ var cachedEls = null;
725
+ function getThemeEls() {
726
+ if (!cachedEls) {
727
+ cachedEls = {
728
+ systemIcon: document.querySelector('.theme-button__icon--system'),
729
+ sunIcon: document.querySelector('.theme-button__icon--sun'),
730
+ moonIcon: document.querySelector('.theme-button__icon--moon'),
731
+ toggle: document.getElementById('theme-toggle')
732
+ };
733
+ }
734
+ return cachedEls;
735
+ }
736
+
737
+ function applyTheme(mode) {
738
+ var resolved = resolveTheme(mode);
739
+ if (resolved === 'light') {
740
+ document.documentElement.setAttribute('data-theme', 'light');
741
+ } else {
742
+ document.documentElement.removeAttribute('data-theme');
743
+ }
744
+
745
+ var els = getThemeEls();
746
+ if (els.systemIcon && els.sunIcon && els.moonIcon) {
747
+ els.systemIcon.classList.add('theme-button__icon--hidden');
748
+ els.sunIcon.classList.add('theme-button__icon--hidden');
749
+ els.moonIcon.classList.add('theme-button__icon--hidden');
750
+ if (mode === 'system') {
751
+ els.systemIcon.classList.remove('theme-button__icon--hidden');
752
+ } else if (mode === 'light') {
753
+ els.sunIcon.classList.remove('theme-button__icon--hidden');
754
+ } else {
755
+ els.moonIcon.classList.remove('theme-button__icon--hidden');
756
+ }
757
+ }
758
+
759
+ if (els.toggle) {
760
+ var labels = { system: 'Theme: system (following system)', light: 'Theme: light', dark: 'Theme: dark' };
761
+ els.toggle.setAttribute('aria-label', labels[mode] || 'Toggle theme');
762
+ }
763
+ }
764
+
765
+ function nextMode(current) {
766
+ var cycle = { system: 'light', light: 'dark', dark: 'system' };
767
+ return cycle[current] || 'system';
768
+ }
769
+
770
+ var currentMode = getMode();
771
+
772
+ document.addEventListener('DOMContentLoaded', function() {
773
+ applyTheme(currentMode);
774
+
775
+ var els = getThemeEls();
776
+ if (els.toggle) {
777
+ els.toggle.addEventListener('click', function() {
778
+ currentMode = nextMode(currentMode);
779
+ localStorage.setItem(themeKey, currentMode);
780
+ applyTheme(currentMode);
781
+ });
782
+ }
783
+ });
784
+
785
+ window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', function() {
786
+ if (getMode() === 'system') {
787
+ applyTheme('system');
788
+ }
789
+ });
790
+ })();
791
+ </script>
792
+ <script defer>
793
+ document.addEventListener('DOMContentLoaded', () => {
794
+ const menuToggle = document.getElementById('mobile-menu-toggle');
795
+ const mobileSidebar = document.getElementById('mobile-sidebar');
796
+ const backdrop = document.getElementById('sidebar-backdrop');
797
+ if (menuToggle && mobileSidebar) {
798
+ menuToggle.addEventListener('click', () => {
799
+ mobileSidebar.classList.toggle('mobile-sidebar--open');
800
+ if (backdrop) backdrop.classList.toggle('sidebar-backdrop--visible');
801
+ });
802
+ if (backdrop) {
803
+ backdrop.addEventListener('click', () => {
804
+ mobileSidebar.classList.remove('mobile-sidebar--open');
805
+ backdrop.classList.remove('sidebar-backdrop--visible');
806
+ });
807
+ }
808
+ }
809
+
810
+ // Page dropdown toggle (docs pages)
811
+ const dropdownToggle = document.getElementById('page-dropdown-toggle');
812
+ const dropdownList = document.getElementById('page-dropdown-list');
813
+ if (dropdownToggle && dropdownList) {
814
+ dropdownToggle.addEventListener('click', () => {
815
+ dropdownList.classList.toggle('page-dropdown__list--open');
816
+ });
817
+ }
818
+
819
+ // Active sidebar link highlighting
820
+ const sidebarLinks = document.querySelectorAll('.nav-sidebar__item');
821
+ const currentPath = window.location.pathname;
822
+ sidebarLinks.forEach((link) => {
823
+ if (link.getAttribute('href') === currentPath || link.getAttribute('href') === currentPath.replace(/\/$/, '')) {
824
+ link.classList.add('nav-sidebar__item--active');
825
+ }
826
+ });
827
+
828
+ // Sidebar section collapse/expand
829
+ const SIDEBAR_STORAGE_KEY = 'docula:sidebar-sections';
830
+ const collapsibleSections = document.querySelectorAll('.nav-sidebar__section--collapsible');
831
+ if (collapsibleSections.length > 0) {
832
+ let storedSectionState = {};
833
+ try {
834
+ storedSectionState = JSON.parse(localStorage.getItem(SIDEBAR_STORAGE_KEY) || '{}');
835
+ } catch (e) { storedSectionState = {}; }
836
+
837
+ const setSectionOpen = (section, toggle, open) => {
838
+ toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
839
+ section.classList.toggle('nav-sidebar__section--collapsed', !open);
840
+ };
841
+
842
+ collapsibleSections.forEach((section, idx) => {
843
+ const toggle = section.querySelector('.nav-sidebar__toggle');
844
+ const list = section.querySelector('.nav-sidebar__list');
845
+ if (!toggle || !list) return;
846
+ const listId = 'nav-sidebar-section-' + idx;
847
+ list.id = listId;
848
+ toggle.setAttribute('aria-controls', listId);
849
+
850
+ const key = 'section-' + idx;
851
+ const hasStored = Object.prototype.hasOwnProperty.call(storedSectionState, key);
852
+ const isOpen = hasStored ? !!storedSectionState[key] : true;
853
+ setSectionOpen(section, toggle, isOpen);
854
+
855
+ toggle.addEventListener('click', () => {
856
+ const next = toggle.getAttribute('aria-expanded') !== 'true';
857
+ setSectionOpen(section, toggle, next);
858
+ storedSectionState[key] = next;
859
+ try {
860
+ localStorage.setItem(SIDEBAR_STORAGE_KEY, JSON.stringify(storedSectionState));
861
+ } catch (e) { /* storage unavailable */ }
862
+ });
863
+ });
864
+ }
865
+
866
+ // Active nav link highlighting (header + mobile). Highlight only the
867
+ // single most-specific link: the one whose path is the longest prefix of
868
+ // the current path. A plain startsWith() check lights up every link whose
869
+ // path merely prefixes the current one, so when the site is embedded under
870
+ // a base path (e.g. served at /developer with docs at that root) the
871
+ // Documentation link — whose path is a prefix of every page — would stay
872
+ // highlighted everywhere. Trailing slashes are normalized so matches stop
873
+ // at a path boundary. We read the anchor's resolved origin/pathname so
874
+ // absolute, relative, and same-origin hrefs all work, while cross-origin
875
+ // links and non-anchor items (e.g. the mobile logout button) are skipped.
876
+ const withTrailingSlash = (p) => (p.endsWith('/') ? p : p + '/');
877
+ const here = withTrailingSlash(currentPath);
878
+ const markActiveNav = (selector, activeClass) => {
879
+ let bestLink = null;
880
+ let bestLength = -1;
881
+ document.querySelectorAll(selector).forEach((link) => {
882
+ // Only same-origin anchors can match the current pathname.
883
+ if (!link.href || link.origin !== window.location.origin) return;
884
+ const base = withTrailingSlash(link.pathname);
885
+ if (here.startsWith(base) && base.length > bestLength) {
886
+ bestLink = link;
887
+ bestLength = base.length;
888
+ }
889
+ });
890
+ if (bestLink) bestLink.classList.add(activeClass);
891
+ };
892
+ markActiveNav('.header-bottom__item', 'header-bottom__item--active');
893
+ markActiveNav('.mobile-nav__item', 'mobile-nav__item--active');
894
+
895
+ // TOC extraction: move the inline TOC into the right sidebar and hide the original
896
+ const tocHeading = document.getElementById('table-of-contents');
897
+ if (tocHeading) {
898
+ const tocList = tocHeading.nextElementSibling;
899
+ const tocTarget = document.getElementById('toc-list');
900
+ if (tocList && tocList.tagName === 'UL' && tocTarget) {
901
+ tocTarget.innerHTML = tocList.innerHTML;
902
+ tocHeading.style.display = 'none';
903
+ tocList.style.display = 'none';
904
+ }
905
+ }
906
+
907
+ // Hide the TOC aside if it has no entries
908
+ const tocSidebar = document.getElementById('toc-list');
909
+ if (tocSidebar && tocSidebar.children.length === 0) {
910
+ const aside = tocSidebar.closest('.content-aside');
911
+ if (aside) { aside.style.display = 'none'; }
912
+ }
913
+
914
+ // Image lightbox
915
+ const lightboxOverlay = document.createElement('div');
916
+ lightboxOverlay.className = 'lightbox-overlay';
917
+ lightboxOverlay.addEventListener('click', function(e) {
918
+ if (e.target !== lightboxImg) lightboxOverlay.classList.remove('lightbox-overlay--visible');
919
+ });
920
+ const lightboxClose = document.createElement('button');
921
+ lightboxClose.className = 'lightbox-close';
922
+ lightboxClose.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
923
+ lightboxClose.addEventListener('click', function() {
924
+ lightboxOverlay.classList.remove('lightbox-overlay--visible');
925
+ });
926
+ const lightboxImg = document.createElement('img');
927
+ lightboxOverlay.appendChild(lightboxClose);
928
+ lightboxOverlay.appendChild(lightboxImg);
929
+ document.body.appendChild(lightboxOverlay);
930
+
931
+ document.querySelectorAll('.article__main img, .changelog-entry-body img').forEach(function(img) {
932
+ if (img.closest('a')) return;
933
+ img.addEventListener('click', function() {
934
+ lightboxImg.src = img.src;
935
+ lightboxOverlay.classList.add('lightbox-overlay--visible');
936
+ });
937
+ });
938
+ });
939
+ </script>
940
+ </body>
941
+
942
+ </html>