@jaredwray/mockhttp 1.9.0 → 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.
- package/README.md +29 -7
- package/dist/index.d.mts +90 -11
- package/dist/index.mjs +202 -240
- package/package.json +17 -13
- package/site/dist/api/index.html +17983 -0
- package/site/dist/api/swagger.json +5487 -0
- package/site/dist/changelog/index.html +767 -0
- package/site/dist/changelog/page/2/index.html +578 -0
- package/site/dist/changelog/v0-1-0/index.html +453 -0
- package/site/dist/changelog/v0-2-0/index.html +437 -0
- package/site/dist/changelog/v0-2-1/index.html +438 -0
- package/site/dist/changelog/v0-2-2/index.html +436 -0
- package/site/dist/changelog/v0-2-3/index.html +440 -0
- package/site/dist/changelog/v0-3-0/index.html +443 -0
- package/site/dist/changelog/v0-4-0/index.html +437 -0
- package/site/dist/changelog/v0-4-1/index.html +441 -0
- package/site/dist/changelog/v0-4-2/index.html +446 -0
- package/site/dist/changelog/v0-4-3/index.html +437 -0
- package/site/dist/changelog/v0-5-0/index.html +443 -0
- package/site/dist/changelog/v0-6-0/index.html +444 -0
- package/site/dist/changelog/v0-6-1/index.html +441 -0
- package/site/dist/changelog/v0-7-0/index.html +445 -0
- package/site/dist/changelog/v0-8-0/index.html +440 -0
- package/site/dist/changelog/v0-9-0/index.html +437 -0
- package/site/dist/changelog/v1-0-0/index.html +446 -0
- package/site/dist/changelog/v1-0-1/index.html +437 -0
- package/site/dist/changelog/v1-1-0/index.html +445 -0
- package/site/dist/changelog/v1-2-0/index.html +443 -0
- package/site/dist/changelog/v1-3-0/index.html +448 -0
- package/site/dist/changelog/v1-4-0/index.html +445 -0
- package/site/dist/changelog/v1-5-0/index.html +439 -0
- package/site/dist/changelog/v1-5-1/index.html +443 -0
- package/site/dist/changelog/v1-6-0/index.html +467 -0
- package/site/dist/changelog/v1-7-0/index.html +462 -0
- package/site/dist/changelog/v1-8-0/index.html +481 -0
- package/site/dist/changelog/v1-8-1/index.html +457 -0
- package/site/dist/changelog/v1-9-0/index.html +495 -0
- package/site/dist/changelog/v2-0-0/index.html +500 -0
- package/site/dist/changelog-latest.json +269 -0
- package/site/dist/changelog.json +399 -0
- package/site/dist/css/api.css +1113 -0
- package/site/dist/css/highlight/highlight.min.js +1433 -0
- package/site/dist/css/highlight/styles/base16/docula.css +123 -0
- package/site/dist/css/home.css +133 -0
- package/site/dist/css/styles.css +1227 -0
- package/site/dist/css/variables.css +32 -0
- package/site/dist/docs/bins/index.html +942 -0
- package/site/dist/docs/configuration/index.html +771 -0
- package/site/dist/docs/hosted/index.html +544 -0
- package/site/dist/docs/https/index.html +758 -0
- package/site/dist/docs/index.html +601 -0
- package/site/dist/docs/library/index.html +780 -0
- package/site/dist/docs/taps/index.html +708 -0
- package/site/dist/favicon.svg +14 -0
- package/site/dist/feed.xml +1 -0
- package/site/dist/index.html +601 -0
- package/site/dist/js/api.js +363 -0
- package/site/dist/js/search.js +436 -0
- package/site/dist/llms-full.txt +7415 -0
- package/site/dist/llms.txt +40 -0
- package/site/dist/logo.svg +21 -0
- package/site/dist/robots.txt +2 -0
- package/site/dist/search-index.json +1 -0
- package/site/dist/sitemap.xml +1 -0
|
@@ -0,0 +1,771 @@
|
|
|
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 - Configuration</title>
|
|
37
|
+
<script type="application/ld+json">
|
|
38
|
+
{"@context":"https://schema.org","@type":"TechArticle","headline":"Configuration","description":"","url":"https://mockhttp.org/docs/configuration/","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">Configuration</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">Configuration</h1>
|
|
255
|
+
</header>
|
|
256
|
+
<div class="article__body">
|
|
257
|
+
<main class="article__main">
|
|
258
|
+
<p>MockHttp supports rate limiting using <a href="https://github.com/fastify/fastify-rate-limit">@fastify/rate-limit</a>. Rate limiting is <strong>enabled by default</strong> at <strong>1000 requests per minute</strong> with <strong>localhost (127.0.0.1 and ::1) excluded</strong> from rate limiting.</p>
|
|
259
|
+
<h2 id="table-of-contents">Table of Contents</h2>
|
|
260
|
+
<ul>
|
|
261
|
+
<li>
|
|
262
|
+
<ul>
|
|
263
|
+
<li><a href="#default-rate-limiting">Default Rate Limiting</a></li>
|
|
264
|
+
<li><a href="#customizing-rate-limiting">Customizing Rate Limiting</a></li>
|
|
265
|
+
<li><a href="#common-configuration-options">Common Configuration Options</a>
|
|
266
|
+
<ul>
|
|
267
|
+
<li><a href="#basic-rate-limiting">Basic Rate Limiting</a></li>
|
|
268
|
+
<li><a href="#stricter-limits-with-custom-error-response">Stricter Limits with Custom Error Response</a></li>
|
|
269
|
+
<li><a href="#allow-list-exclude-specific-ips">Allow List (Exclude Specific IPs)</a></li>
|
|
270
|
+
<li><a href="#custom-key-generator-rate-limit-by-header">Custom Key Generator (Rate Limit by Header)</a></li>
|
|
271
|
+
<li><a href="#advanced-configuration">Advanced Configuration</a></li>
|
|
272
|
+
</ul>
|
|
273
|
+
</li>
|
|
274
|
+
<li><a href="#disabling-rate-limiting">Disabling Rate Limiting</a></li>
|
|
275
|
+
<li><a href="#available-options">Available Options</a></li>
|
|
276
|
+
</ul>
|
|
277
|
+
</li>
|
|
278
|
+
<li><a href="#logging">Logging</a>
|
|
279
|
+
<ul>
|
|
280
|
+
<li><a href="#disabling-logging">Disabling Logging</a></li>
|
|
281
|
+
</ul>
|
|
282
|
+
</li>
|
|
283
|
+
<li><a href="#flexible-url-matching">Flexible URL Matching</a></li>
|
|
284
|
+
</ul>
|
|
285
|
+
<h2 id="default-rate-limiting">Default Rate Limiting</h2>
|
|
286
|
+
<p>By default, MockHttp applies the following rate limit:</p>
|
|
287
|
+
<ul>
|
|
288
|
+
<li><strong>1000 requests per minute</strong> per IP address</li>
|
|
289
|
+
<li><strong>Localhost is excluded</strong> - requests from 127.0.0.1 and ::1 bypass rate limiting (ideal for local development and testing)</li>
|
|
290
|
+
</ul>
|
|
291
|
+
<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>;
|
|
292
|
+
|
|
293
|
+
<span class="hljs-keyword">const</span> mock = <span class="hljs-keyword">new</span> <span class="hljs-title class_">MockHttp</span>();
|
|
294
|
+
<span class="hljs-keyword">await</span> mock.<span class="hljs-title function_">start</span>();
|
|
295
|
+
<span class="hljs-comment">// Rate limiting is active (1000 req/min) except for localhost</span>
|
|
296
|
+
</code></pre>
|
|
297
|
+
<h2 id="customizing-rate-limiting">Customizing Rate Limiting</h2>
|
|
298
|
+
<p>To customize rate limiting, pass a <code>rateLimit</code> configuration object when creating your MockHttp instance:</p>
|
|
299
|
+
<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>;
|
|
300
|
+
|
|
301
|
+
<span class="hljs-keyword">const</span> mock = <span class="hljs-keyword">new</span> <span class="hljs-title class_">MockHttp</span>({
|
|
302
|
+
<span class="hljs-attr">rateLimit</span>: {
|
|
303
|
+
<span class="hljs-attr">max</span>: <span class="hljs-number">100</span>, <span class="hljs-comment">// Maximum 100 requests</span>
|
|
304
|
+
<span class="hljs-attr">timeWindow</span>: <span class="hljs-string">'1 minute'</span> <span class="hljs-comment">// Per 1 minute window</span>
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
<span class="hljs-keyword">await</span> mock.<span class="hljs-title function_">start</span>();
|
|
309
|
+
</code></pre>
|
|
310
|
+
<h2 id="common-configuration-options">Common Configuration Options</h2>
|
|
311
|
+
<p>The <code>rateLimit</code> option accepts all <a href="https://github.com/fastify/fastify-rate-limit#options">@fastify/rate-limit options</a>:</p>
|
|
312
|
+
<h3 id="basic-rate-limiting">Basic Rate Limiting</h3>
|
|
313
|
+
<pre><code class="hljs language-javascript"><span class="hljs-comment">// Limit to 50 requests per minute</span>
|
|
314
|
+
<span class="hljs-keyword">const</span> mock = <span class="hljs-keyword">new</span> <span class="hljs-title class_">MockHttp</span>({
|
|
315
|
+
<span class="hljs-attr">rateLimit</span>: {
|
|
316
|
+
<span class="hljs-attr">max</span>: <span class="hljs-number">50</span>,
|
|
317
|
+
<span class="hljs-attr">timeWindow</span>: <span class="hljs-string">'1 minute'</span>
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
</code></pre>
|
|
321
|
+
<h3 id="stricter-limits-with-custom-error-response">Stricter Limits with Custom Error Response</h3>
|
|
322
|
+
<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>({
|
|
323
|
+
<span class="hljs-attr">rateLimit</span>: {
|
|
324
|
+
<span class="hljs-attr">max</span>: <span class="hljs-number">30</span>,
|
|
325
|
+
<span class="hljs-attr">timeWindow</span>: <span class="hljs-number">60000</span>, <span class="hljs-comment">// 1 minute in milliseconds</span>
|
|
326
|
+
<span class="hljs-attr">errorResponseBuilder</span>: <span class="hljs-function">(<span class="hljs-params">req, context</span>) =></span> ({
|
|
327
|
+
<span class="hljs-attr">statusCode</span>: <span class="hljs-number">429</span>,
|
|
328
|
+
<span class="hljs-attr">error</span>: <span class="hljs-string">'Too Many Requests'</span>,
|
|
329
|
+
<span class="hljs-attr">message</span>: <span class="hljs-string">`Rate limit exceeded. Try again in <span class="hljs-subst">${context.after}</span>`</span>
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
</code></pre>
|
|
334
|
+
<h3 id="allow-list-exclude-specific-ips">Allow List (Exclude Specific IPs)</h3>
|
|
335
|
+
<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>({
|
|
336
|
+
<span class="hljs-attr">rateLimit</span>: {
|
|
337
|
+
<span class="hljs-attr">max</span>: <span class="hljs-number">100</span>,
|
|
338
|
+
<span class="hljs-attr">timeWindow</span>: <span class="hljs-string">'1 minute'</span>,
|
|
339
|
+
<span class="hljs-attr">allowList</span>: [<span class="hljs-string">'127.0.0.1'</span>, <span class="hljs-string">'192.168.1.100'</span>] <span class="hljs-comment">// These IPs bypass rate limiting</span>
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
</code></pre>
|
|
343
|
+
<h3 id="custom-key-generator-rate-limit-by-header">Custom Key Generator (Rate Limit by Header)</h3>
|
|
344
|
+
<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>({
|
|
345
|
+
<span class="hljs-attr">rateLimit</span>: {
|
|
346
|
+
<span class="hljs-attr">max</span>: <span class="hljs-number">100</span>,
|
|
347
|
+
<span class="hljs-attr">timeWindow</span>: <span class="hljs-string">'1 minute'</span>,
|
|
348
|
+
<span class="hljs-attr">keyGenerator</span>: <span class="hljs-function">(<span class="hljs-params">request</span>) =></span> {
|
|
349
|
+
<span class="hljs-comment">// Rate limit by API key instead of IP</span>
|
|
350
|
+
<span class="hljs-keyword">return</span> request.<span class="hljs-property">headers</span>[<span class="hljs-string">'x-api-key'</span>] || request.<span class="hljs-property">ip</span>;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
</code></pre>
|
|
355
|
+
<h3 id="advanced-configuration">Advanced Configuration</h3>
|
|
356
|
+
<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>({
|
|
357
|
+
<span class="hljs-attr">rateLimit</span>: {
|
|
358
|
+
<span class="hljs-attr">global</span>: <span class="hljs-literal">true</span>, <span class="hljs-comment">// Apply to all routes</span>
|
|
359
|
+
<span class="hljs-attr">max</span>: <span class="hljs-number">100</span>, <span class="hljs-comment">// Max requests</span>
|
|
360
|
+
<span class="hljs-attr">timeWindow</span>: <span class="hljs-string">'1 minute'</span>, <span class="hljs-comment">// Time window</span>
|
|
361
|
+
<span class="hljs-attr">cache</span>: <span class="hljs-number">10000</span>, <span class="hljs-comment">// Cache size for tracking clients</span>
|
|
362
|
+
<span class="hljs-attr">skipOnError</span>: <span class="hljs-literal">false</span>, <span class="hljs-comment">// Don't skip on storage errors</span>
|
|
363
|
+
<span class="hljs-attr">ban</span>: <span class="hljs-number">10</span>, <span class="hljs-comment">// Ban after 10 rate limit violations</span>
|
|
364
|
+
<span class="hljs-attr">continueExceeding</span>: <span class="hljs-literal">false</span>, <span class="hljs-comment">// Don't reset window on each request</span>
|
|
365
|
+
<span class="hljs-attr">enableDraftSpec</span>: <span class="hljs-literal">true</span>, <span class="hljs-comment">// Use IETF draft spec headers</span>
|
|
366
|
+
<span class="hljs-attr">addHeaders</span>: { <span class="hljs-comment">// Customize rate limit headers</span>
|
|
367
|
+
<span class="hljs-string">'x-ratelimit-limit'</span>: <span class="hljs-literal">true</span>,
|
|
368
|
+
<span class="hljs-string">'x-ratelimit-remaining'</span>: <span class="hljs-literal">true</span>,
|
|
369
|
+
<span class="hljs-string">'x-ratelimit-reset'</span>: <span class="hljs-literal">true</span>
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
</code></pre>
|
|
374
|
+
<h2 id="disabling-rate-limiting">Disabling Rate Limiting</h2>
|
|
375
|
+
<p>To disable rate limiting completely, set the <code>rateLimit</code> option to <code>false</code>:</p>
|
|
376
|
+
<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>({
|
|
377
|
+
<span class="hljs-attr">rateLimit</span>: <span class="hljs-literal">false</span> <span class="hljs-comment">// Completely disable rate limiting</span>
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
<span class="hljs-keyword">await</span> mock.<span class="hljs-title function_">start</span>();
|
|
381
|
+
<span class="hljs-comment">// No rate limiting is applied to any requests</span>
|
|
382
|
+
</code></pre>
|
|
383
|
+
<p><strong>Note:</strong> To change rate limiting settings after the server has started, you must restart the server:</p>
|
|
384
|
+
<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>();
|
|
385
|
+
<span class="hljs-keyword">await</span> mock.<span class="hljs-title function_">start</span>(); <span class="hljs-comment">// Starts with default rate limiting</span>
|
|
386
|
+
|
|
387
|
+
<span class="hljs-comment">// To change or disable rate limiting:</span>
|
|
388
|
+
<span class="hljs-keyword">await</span> mock.<span class="hljs-title function_">close</span>();
|
|
389
|
+
mock.<span class="hljs-property">rateLimit</span> = <span class="hljs-literal">undefined</span>; <span class="hljs-comment">// or set new options</span>
|
|
390
|
+
<span class="hljs-keyword">await</span> mock.<span class="hljs-title function_">start</span>(); <span class="hljs-comment">// Restarts with new settings</span>
|
|
391
|
+
</code></pre>
|
|
392
|
+
<h2 id="available-options">Available Options</h2>
|
|
393
|
+
<table>
|
|
394
|
+
<thead>
|
|
395
|
+
<tr>
|
|
396
|
+
<th>Option</th>
|
|
397
|
+
<th>Type</th>
|
|
398
|
+
<th>Default</th>
|
|
399
|
+
<th>Description</th>
|
|
400
|
+
</tr>
|
|
401
|
+
</thead>
|
|
402
|
+
<tbody>
|
|
403
|
+
<tr>
|
|
404
|
+
<td><code>max</code></td>
|
|
405
|
+
<td>number | function</td>
|
|
406
|
+
<td><code>1000</code></td>
|
|
407
|
+
<td>Maximum requests per time window</td>
|
|
408
|
+
</tr>
|
|
409
|
+
<tr>
|
|
410
|
+
<td><code>timeWindow</code></td>
|
|
411
|
+
<td>number | string</td>
|
|
412
|
+
<td><code>60000</code></td>
|
|
413
|
+
<td>Duration of rate limit window (milliseconds or string like '1 minute')</td>
|
|
414
|
+
</tr>
|
|
415
|
+
<tr>
|
|
416
|
+
<td><code>cache</code></td>
|
|
417
|
+
<td>number</td>
|
|
418
|
+
<td><code>5000</code></td>
|
|
419
|
+
<td>LRU cache size for tracking clients</td>
|
|
420
|
+
</tr>
|
|
421
|
+
<tr>
|
|
422
|
+
<td><code>allowList</code></td>
|
|
423
|
+
<td>array | function</td>
|
|
424
|
+
<td><code>[]</code></td>
|
|
425
|
+
<td>IPs or function to exclude from rate limiting</td>
|
|
426
|
+
</tr>
|
|
427
|
+
<tr>
|
|
428
|
+
<td><code>keyGenerator</code></td>
|
|
429
|
+
<td>function</td>
|
|
430
|
+
<td>IP-based</td>
|
|
431
|
+
<td>Function to generate unique client identifier</td>
|
|
432
|
+
</tr>
|
|
433
|
+
<tr>
|
|
434
|
+
<td><code>errorResponseBuilder</code></td>
|
|
435
|
+
<td>function</td>
|
|
436
|
+
<td>Default 429</td>
|
|
437
|
+
<td>Custom error response function</td>
|
|
438
|
+
</tr>
|
|
439
|
+
<tr>
|
|
440
|
+
<td><code>skipOnError</code></td>
|
|
441
|
+
<td>boolean</td>
|
|
442
|
+
<td><code>false</code></td>
|
|
443
|
+
<td>Skip rate limiting if storage errors occur</td>
|
|
444
|
+
</tr>
|
|
445
|
+
<tr>
|
|
446
|
+
<td><code>ban</code></td>
|
|
447
|
+
<td>number</td>
|
|
448
|
+
<td><code>-1</code></td>
|
|
449
|
+
<td>Ban client after N violations (disabled by default)</td>
|
|
450
|
+
</tr>
|
|
451
|
+
<tr>
|
|
452
|
+
<td><code>continueExceeding</code></td>
|
|
453
|
+
<td>boolean</td>
|
|
454
|
+
<td><code>false</code></td>
|
|
455
|
+
<td>Renew time window on each request while limited</td>
|
|
456
|
+
</tr>
|
|
457
|
+
<tr>
|
|
458
|
+
<td><code>enableDraftSpec</code></td>
|
|
459
|
+
<td>boolean</td>
|
|
460
|
+
<td><code>false</code></td>
|
|
461
|
+
<td>Use IETF draft specification headers</td>
|
|
462
|
+
</tr>
|
|
463
|
+
</tbody>
|
|
464
|
+
</table>
|
|
465
|
+
<p>For the complete list of options, see the <a href="https://github.com/fastify/fastify-rate-limit#options">@fastify/rate-limit documentation</a>.</p>
|
|
466
|
+
<h1 id="logging">Logging</h1>
|
|
467
|
+
<p>MockHttp uses <a href="https://github.com/pinojs/pino">Pino</a> for logging via Fastify's built-in logger. Logging is <strong>enabled by default</strong> but can be disabled when needed.</p>
|
|
468
|
+
<h2 id="disabling-logging">Disabling Logging</h2>
|
|
469
|
+
<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>;
|
|
470
|
+
|
|
471
|
+
<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> });
|
|
472
|
+
<span class="hljs-keyword">await</span> mock.<span class="hljs-title function_">start</span>();
|
|
473
|
+
<span class="hljs-comment">// Server runs silently without any log output</span>
|
|
474
|
+
</code></pre>
|
|
475
|
+
<p>You can also disable logging via the <code>LOGGING</code> environment variable:</p>
|
|
476
|
+
<pre><code class="hljs language-bash">LOGGING=<span class="hljs-literal">false</span> node your-app.js
|
|
477
|
+
</code></pre>
|
|
478
|
+
<h1 id="flexible-url-matching">Flexible URL Matching</h1>
|
|
479
|
+
<p>MockHttp ignores trailing path segments that come after the parsable portion of a URL. This is useful when a client appends extra data to a known endpoint — instead of returning 404, MockHttp serves the closest matching route.</p>
|
|
480
|
+
<p>For example, all of these are served by <code>/status/:code</code>:</p>
|
|
481
|
+
<pre><code>GET /status/429
|
|
482
|
+
GET /status/429/
|
|
483
|
+
GET /status/429/foo
|
|
484
|
+
GET /status/429/foo/bar
|
|
485
|
+
</code></pre>
|
|
486
|
+
<p>The rewrite preserves the query string and only triggers when a more specific route exists; URLs whose first path segment doesn't correspond to a registered route still return 404.</p>
|
|
487
|
+
</main>
|
|
488
|
+
<aside class="content-aside">
|
|
489
|
+
<h2 class="content-aside__title">TABLE OF CONTENTS</h2>
|
|
490
|
+
<ul class="content-aside__list" id="toc-list">
|
|
491
|
+
</ul>
|
|
492
|
+
</aside>
|
|
493
|
+
</div>
|
|
494
|
+
<a class="article__edit-link" href="https://github.com/jaredwray/mockhttp/edit/main/site/docs/configuration.md" target="_blank" rel="noopener noreferrer">
|
|
495
|
+
<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>
|
|
496
|
+
Edit this page
|
|
497
|
+
</a>
|
|
498
|
+
</article>
|
|
499
|
+
</div>
|
|
500
|
+
<footer>
|
|
501
|
+
<a href="/llms.txt" class="footer__link">llms.txt</a>
|
|
502
|
+
<a href="/feed.xml" class="footer__link" aria-label="RSS Feed">
|
|
503
|
+
<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>
|
|
504
|
+
</a>
|
|
505
|
+
|
|
506
|
+
<a href="https://docula.org/" target="_blank" rel="noopener noreferrer" class="footer__link" aria-label="Docula">
|
|
507
|
+
<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>
|
|
508
|
+
</a>
|
|
509
|
+
<span class="footer__updated">updated: 2026-09-18</span>
|
|
510
|
+
</footer>
|
|
511
|
+
<script src="/css/highlight/highlight.min.js"></script>
|
|
512
|
+
<script>
|
|
513
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
514
|
+
document.querySelectorAll('pre code').forEach(el => { el.textContent = el.textContent.trimEnd(); });
|
|
515
|
+
hljs.highlightAll();
|
|
516
|
+
var kbd = document.querySelector('.search-button__shortcut');
|
|
517
|
+
if (kbd) kbd.textContent = navigator.platform.indexOf('Mac') > -1 ? '⌘K' : 'Ctrl K';
|
|
518
|
+
|
|
519
|
+
// Copy code buttons
|
|
520
|
+
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>';
|
|
521
|
+
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>';
|
|
522
|
+
document.querySelectorAll('pre').forEach(function(pre) {
|
|
523
|
+
const btn = document.createElement('button');
|
|
524
|
+
btn.className = 'copy-code-btn';
|
|
525
|
+
btn.innerHTML = copyIcon;
|
|
526
|
+
btn.setAttribute('aria-label', 'Copy code');
|
|
527
|
+
btn.addEventListener('click', function() {
|
|
528
|
+
const code = pre.querySelector('code');
|
|
529
|
+
const text = code ? code.textContent : pre.textContent;
|
|
530
|
+
navigator.clipboard.writeText(text || '').then(function() {
|
|
531
|
+
btn.innerHTML = checkIcon;
|
|
532
|
+
setTimeout(function() { btn.innerHTML = copyIcon; }, 2000);
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
pre.appendChild(btn);
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
</script>
|
|
539
|
+
<script>
|
|
540
|
+
(function() {
|
|
541
|
+
var themeKey = window.__doculaThemeKey;
|
|
542
|
+
function getMode() {
|
|
543
|
+
return localStorage.getItem(themeKey) || 'light';
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function resolveTheme(mode) {
|
|
547
|
+
if (mode === 'system') {
|
|
548
|
+
return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
|
549
|
+
}
|
|
550
|
+
return mode;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
var cachedEls = null;
|
|
554
|
+
function getThemeEls() {
|
|
555
|
+
if (!cachedEls) {
|
|
556
|
+
cachedEls = {
|
|
557
|
+
systemIcon: document.querySelector('.theme-button__icon--system'),
|
|
558
|
+
sunIcon: document.querySelector('.theme-button__icon--sun'),
|
|
559
|
+
moonIcon: document.querySelector('.theme-button__icon--moon'),
|
|
560
|
+
toggle: document.getElementById('theme-toggle')
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
return cachedEls;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function applyTheme(mode) {
|
|
567
|
+
var resolved = resolveTheme(mode);
|
|
568
|
+
if (resolved === 'light') {
|
|
569
|
+
document.documentElement.setAttribute('data-theme', 'light');
|
|
570
|
+
} else {
|
|
571
|
+
document.documentElement.removeAttribute('data-theme');
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
var els = getThemeEls();
|
|
575
|
+
if (els.systemIcon && els.sunIcon && els.moonIcon) {
|
|
576
|
+
els.systemIcon.classList.add('theme-button__icon--hidden');
|
|
577
|
+
els.sunIcon.classList.add('theme-button__icon--hidden');
|
|
578
|
+
els.moonIcon.classList.add('theme-button__icon--hidden');
|
|
579
|
+
if (mode === 'system') {
|
|
580
|
+
els.systemIcon.classList.remove('theme-button__icon--hidden');
|
|
581
|
+
} else if (mode === 'light') {
|
|
582
|
+
els.sunIcon.classList.remove('theme-button__icon--hidden');
|
|
583
|
+
} else {
|
|
584
|
+
els.moonIcon.classList.remove('theme-button__icon--hidden');
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (els.toggle) {
|
|
589
|
+
var labels = { system: 'Theme: system (following system)', light: 'Theme: light', dark: 'Theme: dark' };
|
|
590
|
+
els.toggle.setAttribute('aria-label', labels[mode] || 'Toggle theme');
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function nextMode(current) {
|
|
595
|
+
var cycle = { system: 'light', light: 'dark', dark: 'system' };
|
|
596
|
+
return cycle[current] || 'system';
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
var currentMode = getMode();
|
|
600
|
+
|
|
601
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
602
|
+
applyTheme(currentMode);
|
|
603
|
+
|
|
604
|
+
var els = getThemeEls();
|
|
605
|
+
if (els.toggle) {
|
|
606
|
+
els.toggle.addEventListener('click', function() {
|
|
607
|
+
currentMode = nextMode(currentMode);
|
|
608
|
+
localStorage.setItem(themeKey, currentMode);
|
|
609
|
+
applyTheme(currentMode);
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', function() {
|
|
615
|
+
if (getMode() === 'system') {
|
|
616
|
+
applyTheme('system');
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
})();
|
|
620
|
+
</script>
|
|
621
|
+
<script defer>
|
|
622
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
623
|
+
const menuToggle = document.getElementById('mobile-menu-toggle');
|
|
624
|
+
const mobileSidebar = document.getElementById('mobile-sidebar');
|
|
625
|
+
const backdrop = document.getElementById('sidebar-backdrop');
|
|
626
|
+
if (menuToggle && mobileSidebar) {
|
|
627
|
+
menuToggle.addEventListener('click', () => {
|
|
628
|
+
mobileSidebar.classList.toggle('mobile-sidebar--open');
|
|
629
|
+
if (backdrop) backdrop.classList.toggle('sidebar-backdrop--visible');
|
|
630
|
+
});
|
|
631
|
+
if (backdrop) {
|
|
632
|
+
backdrop.addEventListener('click', () => {
|
|
633
|
+
mobileSidebar.classList.remove('mobile-sidebar--open');
|
|
634
|
+
backdrop.classList.remove('sidebar-backdrop--visible');
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Page dropdown toggle (docs pages)
|
|
640
|
+
const dropdownToggle = document.getElementById('page-dropdown-toggle');
|
|
641
|
+
const dropdownList = document.getElementById('page-dropdown-list');
|
|
642
|
+
if (dropdownToggle && dropdownList) {
|
|
643
|
+
dropdownToggle.addEventListener('click', () => {
|
|
644
|
+
dropdownList.classList.toggle('page-dropdown__list--open');
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Active sidebar link highlighting
|
|
649
|
+
const sidebarLinks = document.querySelectorAll('.nav-sidebar__item');
|
|
650
|
+
const currentPath = window.location.pathname;
|
|
651
|
+
sidebarLinks.forEach((link) => {
|
|
652
|
+
if (link.getAttribute('href') === currentPath || link.getAttribute('href') === currentPath.replace(/\/$/, '')) {
|
|
653
|
+
link.classList.add('nav-sidebar__item--active');
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
// Sidebar section collapse/expand
|
|
658
|
+
const SIDEBAR_STORAGE_KEY = 'docula:sidebar-sections';
|
|
659
|
+
const collapsibleSections = document.querySelectorAll('.nav-sidebar__section--collapsible');
|
|
660
|
+
if (collapsibleSections.length > 0) {
|
|
661
|
+
let storedSectionState = {};
|
|
662
|
+
try {
|
|
663
|
+
storedSectionState = JSON.parse(localStorage.getItem(SIDEBAR_STORAGE_KEY) || '{}');
|
|
664
|
+
} catch (e) { storedSectionState = {}; }
|
|
665
|
+
|
|
666
|
+
const setSectionOpen = (section, toggle, open) => {
|
|
667
|
+
toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
668
|
+
section.classList.toggle('nav-sidebar__section--collapsed', !open);
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
collapsibleSections.forEach((section, idx) => {
|
|
672
|
+
const toggle = section.querySelector('.nav-sidebar__toggle');
|
|
673
|
+
const list = section.querySelector('.nav-sidebar__list');
|
|
674
|
+
if (!toggle || !list) return;
|
|
675
|
+
const listId = 'nav-sidebar-section-' + idx;
|
|
676
|
+
list.id = listId;
|
|
677
|
+
toggle.setAttribute('aria-controls', listId);
|
|
678
|
+
|
|
679
|
+
const key = 'section-' + idx;
|
|
680
|
+
const hasStored = Object.prototype.hasOwnProperty.call(storedSectionState, key);
|
|
681
|
+
const isOpen = hasStored ? !!storedSectionState[key] : true;
|
|
682
|
+
setSectionOpen(section, toggle, isOpen);
|
|
683
|
+
|
|
684
|
+
toggle.addEventListener('click', () => {
|
|
685
|
+
const next = toggle.getAttribute('aria-expanded') !== 'true';
|
|
686
|
+
setSectionOpen(section, toggle, next);
|
|
687
|
+
storedSectionState[key] = next;
|
|
688
|
+
try {
|
|
689
|
+
localStorage.setItem(SIDEBAR_STORAGE_KEY, JSON.stringify(storedSectionState));
|
|
690
|
+
} catch (e) { /* storage unavailable */ }
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Active nav link highlighting (header + mobile). Highlight only the
|
|
696
|
+
// single most-specific link: the one whose path is the longest prefix of
|
|
697
|
+
// the current path. A plain startsWith() check lights up every link whose
|
|
698
|
+
// path merely prefixes the current one, so when the site is embedded under
|
|
699
|
+
// a base path (e.g. served at /developer with docs at that root) the
|
|
700
|
+
// Documentation link — whose path is a prefix of every page — would stay
|
|
701
|
+
// highlighted everywhere. Trailing slashes are normalized so matches stop
|
|
702
|
+
// at a path boundary. We read the anchor's resolved origin/pathname so
|
|
703
|
+
// absolute, relative, and same-origin hrefs all work, while cross-origin
|
|
704
|
+
// links and non-anchor items (e.g. the mobile logout button) are skipped.
|
|
705
|
+
const withTrailingSlash = (p) => (p.endsWith('/') ? p : p + '/');
|
|
706
|
+
const here = withTrailingSlash(currentPath);
|
|
707
|
+
const markActiveNav = (selector, activeClass) => {
|
|
708
|
+
let bestLink = null;
|
|
709
|
+
let bestLength = -1;
|
|
710
|
+
document.querySelectorAll(selector).forEach((link) => {
|
|
711
|
+
// Only same-origin anchors can match the current pathname.
|
|
712
|
+
if (!link.href || link.origin !== window.location.origin) return;
|
|
713
|
+
const base = withTrailingSlash(link.pathname);
|
|
714
|
+
if (here.startsWith(base) && base.length > bestLength) {
|
|
715
|
+
bestLink = link;
|
|
716
|
+
bestLength = base.length;
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
if (bestLink) bestLink.classList.add(activeClass);
|
|
720
|
+
};
|
|
721
|
+
markActiveNav('.header-bottom__item', 'header-bottom__item--active');
|
|
722
|
+
markActiveNav('.mobile-nav__item', 'mobile-nav__item--active');
|
|
723
|
+
|
|
724
|
+
// TOC extraction: move the inline TOC into the right sidebar and hide the original
|
|
725
|
+
const tocHeading = document.getElementById('table-of-contents');
|
|
726
|
+
if (tocHeading) {
|
|
727
|
+
const tocList = tocHeading.nextElementSibling;
|
|
728
|
+
const tocTarget = document.getElementById('toc-list');
|
|
729
|
+
if (tocList && tocList.tagName === 'UL' && tocTarget) {
|
|
730
|
+
tocTarget.innerHTML = tocList.innerHTML;
|
|
731
|
+
tocHeading.style.display = 'none';
|
|
732
|
+
tocList.style.display = 'none';
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Hide the TOC aside if it has no entries
|
|
737
|
+
const tocSidebar = document.getElementById('toc-list');
|
|
738
|
+
if (tocSidebar && tocSidebar.children.length === 0) {
|
|
739
|
+
const aside = tocSidebar.closest('.content-aside');
|
|
740
|
+
if (aside) { aside.style.display = 'none'; }
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Image lightbox
|
|
744
|
+
const lightboxOverlay = document.createElement('div');
|
|
745
|
+
lightboxOverlay.className = 'lightbox-overlay';
|
|
746
|
+
lightboxOverlay.addEventListener('click', function(e) {
|
|
747
|
+
if (e.target !== lightboxImg) lightboxOverlay.classList.remove('lightbox-overlay--visible');
|
|
748
|
+
});
|
|
749
|
+
const lightboxClose = document.createElement('button');
|
|
750
|
+
lightboxClose.className = 'lightbox-close';
|
|
751
|
+
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>';
|
|
752
|
+
lightboxClose.addEventListener('click', function() {
|
|
753
|
+
lightboxOverlay.classList.remove('lightbox-overlay--visible');
|
|
754
|
+
});
|
|
755
|
+
const lightboxImg = document.createElement('img');
|
|
756
|
+
lightboxOverlay.appendChild(lightboxClose);
|
|
757
|
+
lightboxOverlay.appendChild(lightboxImg);
|
|
758
|
+
document.body.appendChild(lightboxOverlay);
|
|
759
|
+
|
|
760
|
+
document.querySelectorAll('.article__main img, .changelog-entry-body img').forEach(function(img) {
|
|
761
|
+
if (img.closest('a')) return;
|
|
762
|
+
img.addEventListener('click', function() {
|
|
763
|
+
lightboxImg.src = img.src;
|
|
764
|
+
lightboxOverlay.classList.add('lightbox-overlay--visible');
|
|
765
|
+
});
|
|
766
|
+
});
|
|
767
|
+
});
|
|
768
|
+
</script>
|
|
769
|
+
</body>
|
|
770
|
+
|
|
771
|
+
</html>
|