zero-http 0.2.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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +311 -0
  3. package/documentation/controllers/cleanup.js +25 -0
  4. package/documentation/controllers/echo.js +14 -0
  5. package/documentation/controllers/headers.js +1 -0
  6. package/documentation/controllers/proxy.js +112 -0
  7. package/documentation/controllers/root.js +1 -0
  8. package/documentation/controllers/uploads.js +289 -0
  9. package/documentation/full-server.js +129 -0
  10. package/documentation/public/data/api.json +167 -0
  11. package/documentation/public/data/examples.json +62 -0
  12. package/documentation/public/data/options.json +13 -0
  13. package/documentation/public/index.html +414 -0
  14. package/documentation/public/prism-overrides.css +40 -0
  15. package/documentation/public/scripts/app.js +44 -0
  16. package/documentation/public/scripts/data-sections.js +300 -0
  17. package/documentation/public/scripts/helpers.js +166 -0
  18. package/documentation/public/scripts/playground.js +71 -0
  19. package/documentation/public/scripts/proxy.js +98 -0
  20. package/documentation/public/scripts/ui.js +210 -0
  21. package/documentation/public/scripts/uploads.js +459 -0
  22. package/documentation/public/styles.css +310 -0
  23. package/documentation/public/vendor/icons/fetch.svg +23 -0
  24. package/documentation/public/vendor/icons/plug.svg +27 -0
  25. package/documentation/public/vendor/icons/static.svg +35 -0
  26. package/documentation/public/vendor/icons/stream.svg +22 -0
  27. package/documentation/public/vendor/icons/zero.svg +21 -0
  28. package/documentation/public/vendor/prism-copy-to-clipboard.min.js +27 -0
  29. package/documentation/public/vendor/prism-javascript.min.js +1 -0
  30. package/documentation/public/vendor/prism-json.min.js +1 -0
  31. package/documentation/public/vendor/prism-okaidia.css +1 -0
  32. package/documentation/public/vendor/prism-toolbar.css +27 -0
  33. package/documentation/public/vendor/prism-toolbar.min.js +41 -0
  34. package/documentation/public/vendor/prism.min.js +1 -0
  35. package/index.js +43 -0
  36. package/lib/app.js +159 -0
  37. package/lib/body/index.js +14 -0
  38. package/lib/body/json.js +54 -0
  39. package/lib/body/multipart.js +310 -0
  40. package/lib/body/raw.js +40 -0
  41. package/lib/body/rawBuffer.js +74 -0
  42. package/lib/body/sendError.js +17 -0
  43. package/lib/body/text.js +43 -0
  44. package/lib/body/typeMatch.js +22 -0
  45. package/lib/body/urlencoded.js +166 -0
  46. package/lib/cors.js +72 -0
  47. package/lib/fetch.js +218 -0
  48. package/lib/logger.js +68 -0
  49. package/lib/rateLimit.js +64 -0
  50. package/lib/request.js +76 -0
  51. package/lib/response.js +165 -0
  52. package/lib/router.js +87 -0
  53. package/lib/static.js +196 -0
  54. package/package.json +44 -0
@@ -0,0 +1,300 @@
1
+ /**
2
+ * data-sections.js
3
+ * Fetches and renders the three data-driven documentation sections:
4
+ * - API Reference (/data/api.json)
5
+ * - Options Table (/data/options.json)
6
+ * - Code Examples (/data/examples.json)
7
+ *
8
+ * Also populates the sidebar TOC with sub-items for the API reference and
9
+ * examples sections.
10
+ *
11
+ * Depends on: helpers.js (provides $, escapeHtml, slugify, showJsonResult,
12
+ * highlightAllPre)
13
+ */
14
+
15
+ /* ── TOC Helpers ───────────────────────────────────────────────────────────── */
16
+
17
+ /**
18
+ * Find a top-level `<li>` in the sidebar TOC whose link matches the given
19
+ * href, then append a sub-list of items beneath it.
20
+ * @param {string} href - Hash href to match (e.g. "#api-reference").
21
+ * @param {Object[]} items - Array of `{ slug, label }` objects.
22
+ */
23
+ function populateTocSub(href, items)
24
+ {
25
+ const nav = document.querySelector('.toc-sidebar nav ul');
26
+ if (!nav || !items || !items.length) return;
27
+
28
+ const parentLi = Array.from(nav.children).find(li =>
29
+ {
30
+ const a = li.querySelector && li.querySelector(`a[href="${href}"]`);
31
+ return !!a;
32
+ });
33
+ if (!parentLi) return;
34
+
35
+ /* Remove existing sub-list if present (re-render safe) */
36
+ const existing = parentLi.querySelector('ul.toc-sub');
37
+ if (existing) existing.remove();
38
+
39
+ const sub = document.createElement('ul');
40
+ sub.className = 'toc-sub';
41
+
42
+ for (const { slug, label } of items)
43
+ {
44
+ const li = document.createElement('li');
45
+ li.className = 'toc-sub-item';
46
+
47
+ const a = document.createElement('a');
48
+ a.href = '#' + slug;
49
+ a.textContent = label;
50
+ a.addEventListener('click', () => document.body.classList.remove('toc-open'));
51
+
52
+ li.appendChild(a);
53
+ sub.appendChild(li);
54
+ }
55
+
56
+ parentLi.appendChild(sub);
57
+ }
58
+
59
+ /* ── API Reference ─────────────────────────────────────────────────────────── */
60
+
61
+ /**
62
+ * Render a list of API items into the `#api-items` container and Prism-highlight
63
+ * any code examples.
64
+ * @param {HTMLElement} container - Target element.
65
+ * @param {Object[]} list - API item descriptors.
66
+ */
67
+ function renderApiList(container, list)
68
+ {
69
+ container.innerHTML = '';
70
+ if (!list || !list.length) { container.textContent = 'No API items'; return; }
71
+
72
+ for (const it of list)
73
+ {
74
+ const d = document.createElement('details');
75
+ d.className = 'acc nested';
76
+ d.id = 'api-' + slugify(it.name || '');
77
+
78
+ const s = document.createElement('summary');
79
+ s.innerHTML = `<strong>${escapeHtml(it.name)}</strong>`;
80
+ d.appendChild(s);
81
+
82
+ const body = document.createElement('div');
83
+ body.className = 'acc-body';
84
+
85
+ /* Description */
86
+ if (it.description)
87
+ {
88
+ const p = document.createElement('p');
89
+ p.innerHTML = escapeHtml(it.description);
90
+ body.appendChild(p);
91
+ }
92
+
93
+ /* Options table */
94
+ if (Array.isArray(it.options) && it.options.length)
95
+ {
96
+ const table = document.createElement('table');
97
+ table.innerHTML = '<thead><tr><th>Option</th><th>Type</th><th>Default</th><th>Notes</th></tr></thead>';
98
+ const tbody = document.createElement('tbody');
99
+ for (const opt of it.options)
100
+ {
101
+ const tr = document.createElement('tr');
102
+ tr.innerHTML =
103
+ `<td><code>${escapeHtml(opt.option)}</code></td>` +
104
+ `<td>${escapeHtml(opt.type || '')}</td>` +
105
+ `<td>${escapeHtml(opt.default || '')}</td>` +
106
+ `<td>${escapeHtml(opt.notes || '')}</td>`;
107
+ tbody.appendChild(tr);
108
+ }
109
+ table.appendChild(tbody);
110
+ body.appendChild(table);
111
+ }
112
+
113
+ /* Methods table */
114
+ if (Array.isArray(it.methods) && it.methods.length)
115
+ {
116
+ const table = document.createElement('table');
117
+ table.innerHTML = '<thead><tr><th>Method</th><th>Signature</th><th>Description</th></tr></thead>';
118
+ const tbody = document.createElement('tbody');
119
+ for (const m of it.methods)
120
+ {
121
+ const tr = document.createElement('tr');
122
+ tr.innerHTML =
123
+ `<td><code>${escapeHtml(m.method || '')}</code></td>` +
124
+ `<td><code>${escapeHtml(m.signature || '')}</code></td>` +
125
+ `<td>${escapeHtml(m.description || '')}</td>`;
126
+ tbody.appendChild(tr);
127
+ }
128
+ table.appendChild(tbody);
129
+ body.appendChild(table);
130
+ }
131
+
132
+ /* Example code block */
133
+ if (it.example)
134
+ {
135
+ const h6 = document.createElement('h6');
136
+ h6.textContent = 'Example';
137
+ const pre = document.createElement('pre');
138
+ pre.className = 'language-javascript code';
139
+ const code = document.createElement('code');
140
+ code.className = 'language-javascript';
141
+ code.textContent = it.example;
142
+ pre.appendChild(code);
143
+ body.appendChild(h6);
144
+ body.appendChild(pre);
145
+ }
146
+
147
+ d.appendChild(body);
148
+ container.appendChild(d);
149
+ }
150
+
151
+ try { highlightAllPre(); } catch (e) { }
152
+ }
153
+
154
+ /**
155
+ * Fetch the API reference JSON, render it, populate the sidebar TOC, and wire
156
+ * the search / clear filter controls.
157
+ */
158
+ async function loadApiReference()
159
+ {
160
+ try
161
+ {
162
+ const res = await fetch('/data/api.json', { cache: 'no-store' });
163
+ if (!res.ok) return;
164
+
165
+ const items = await res.json();
166
+ const container = document.getElementById('api-items');
167
+ if (!container) return;
168
+
169
+ window._apiItems = items;
170
+ renderApiList(container, items);
171
+
172
+ /* Sidebar TOC sub-items */
173
+ populateTocSub('#api-reference', items.map(it => ({
174
+ slug: 'api-' + slugify(it.name),
175
+ label: it.name || '',
176
+ })));
177
+
178
+ /* Search / clear filter */
179
+ const search = document.getElementById('api-search');
180
+ const clearBtn = document.getElementById('api-clear');
181
+
182
+ const doFilter = () =>
183
+ {
184
+ const q = (search && search.value || '').trim().toLowerCase();
185
+ if (!q) return renderApiList(container, window._apiItems);
186
+ const filtered = window._apiItems.filter(it =>
187
+ (it.name || '').toLowerCase().includes(q) ||
188
+ (it.description || '').toLowerCase().includes(q) ||
189
+ JSON.stringify(it.options || []).toLowerCase().includes(q)
190
+ );
191
+ renderApiList(container, filtered);
192
+ };
193
+
194
+ if (search) search.addEventListener('input', doFilter);
195
+ if (clearBtn) clearBtn.addEventListener('click', () => { if (search) search.value = ''; renderApiList(container, window._apiItems); });
196
+ } catch (e) { }
197
+ }
198
+
199
+ /* ── Options Table ─────────────────────────────────────────────────────────── */
200
+
201
+ /**
202
+ * Fetch the options JSON and render a `<table>` into `#options-items`.
203
+ */
204
+ async function loadOptions()
205
+ {
206
+ try
207
+ {
208
+ const container = document.getElementById('options-items');
209
+ if (!container) return;
210
+
211
+ const res = await fetch('/data/options.json', { cache: 'no-store' });
212
+ if (!res.ok) { container.textContent = 'Error loading options: ' + res.status + ' ' + res.statusText; return; }
213
+
214
+ let items;
215
+ try { items = await res.json(); }
216
+ catch (err) { container.textContent = 'Error parsing options JSON'; return; }
217
+
218
+ const table = document.createElement('table');
219
+ table.innerHTML = '<thead><tr><th>Option</th><th>Type</th><th>Default</th><th>Notes</th></tr></thead>';
220
+ const tbody = document.createElement('tbody');
221
+
222
+ for (const it of items)
223
+ {
224
+ const tr = document.createElement('tr');
225
+ tr.innerHTML =
226
+ `<td><strong>${escapeHtml(it.option || '')}</strong></td>` +
227
+ `<td>${escapeHtml(it.type || '')}</td>` +
228
+ `<td>${escapeHtml(it.default || '')}</td>` +
229
+ `<td>${escapeHtml(it.notes || it.description || '')}</td>`;
230
+ tbody.appendChild(tr);
231
+ }
232
+
233
+ table.appendChild(tbody);
234
+ container.innerHTML = '';
235
+ container.appendChild(table);
236
+ } catch (e) { console.error('loadOptions error', e); }
237
+ }
238
+
239
+ /* ── Code Examples ─────────────────────────────────────────────────────────── */
240
+
241
+ /**
242
+ * Fetch the examples JSON, render each as a collapsible accordion, and
243
+ * populate the sidebar TOC.
244
+ */
245
+ async function loadExamples()
246
+ {
247
+ try
248
+ {
249
+ const container = document.getElementById('examples-items');
250
+ if (!container) return;
251
+
252
+ const res = await fetch('/data/examples.json', { cache: 'no-store' });
253
+ if (!res.ok) { container.textContent = 'Error loading examples: ' + res.status; return; }
254
+
255
+ const items = await res.json();
256
+ container.innerHTML = '';
257
+
258
+ for (const it of items)
259
+ {
260
+ const id = 'example-' + slugify(it.title);
261
+ const d = document.createElement('details');
262
+ d.className = 'acc nested';
263
+ d.id = id;
264
+
265
+ const s = document.createElement('summary');
266
+ s.innerHTML = `<strong>${escapeHtml(it.title || '')}</strong>`;
267
+ d.appendChild(s);
268
+
269
+ const body = document.createElement('div');
270
+ body.className = 'acc-body';
271
+
272
+ if (it.description)
273
+ {
274
+ const p = document.createElement('p');
275
+ p.className = 'muted';
276
+ p.textContent = it.description;
277
+ body.appendChild(p);
278
+ }
279
+
280
+ const pre = document.createElement('pre');
281
+ pre.className = it.language ? 'language-' + it.language + ' code' : 'code';
282
+ const code = document.createElement('code');
283
+ if (it.language) code.className = 'language-' + it.language;
284
+ code.textContent = it.code || '';
285
+ pre.appendChild(code);
286
+ body.appendChild(pre);
287
+
288
+ d.appendChild(body);
289
+ container.appendChild(d);
290
+ }
291
+
292
+ try { highlightAllPre(); } catch (e) { }
293
+
294
+ /* Sidebar TOC sub-items */
295
+ populateTocSub('#simple-examples', items.map(it => ({
296
+ slug: 'example-' + slugify(it.title),
297
+ label: it.title || '',
298
+ })));
299
+ } catch (e) { console.error('loadExamples error', e); }
300
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * helpers.js
3
+ * Shared DOM selectors, formatters, and rendering utilities used across all
4
+ * documentation scripts. Loaded first so every other module can rely on these.
5
+ */
6
+
7
+ /* ── DOM Selectors ─────────────────────────────────────────────────────────── */
8
+
9
+ /**
10
+ * Query a single element by CSS selector.
11
+ * @param {string} sel - CSS selector string.
12
+ * @param {Element} ctx - Optional root element (defaults to `document`).
13
+ * @returns {Element|null}
14
+ */
15
+ const $ = (sel, ctx = document) => ctx.querySelector(sel);
16
+
17
+ /**
18
+ * Query all matching elements and return a real array.
19
+ * @param {string} sel - CSS selector string.
20
+ * @param {Element} ctx - Optional root element (defaults to `document`).
21
+ * @returns {Element[]}
22
+ */
23
+ const $$ = (sel, ctx = document) => Array.from((ctx || document).querySelectorAll(sel));
24
+
25
+ /**
26
+ * Attach an event listener, safely no-ops if element is null.
27
+ * @param {Element|null} el - Target element.
28
+ * @param {string} evt - Event name.
29
+ * @param {Function} cb - Callback.
30
+ */
31
+ function on(el, evt, cb)
32
+ {
33
+ if (!el) return;
34
+ el.addEventListener(evt, cb);
35
+ }
36
+
37
+ /* ── String Utilities ──────────────────────────────────────────────────────── */
38
+
39
+ /**
40
+ * Escape HTML special characters for safe insertion into the DOM.
41
+ * @param {string} s - Raw string.
42
+ * @returns {string} Escaped string.
43
+ */
44
+ function escapeHtml(s)
45
+ {
46
+ return s
47
+ .replace(/&/g, '&amp;')
48
+ .replace(/</g, '&lt;')
49
+ .replace(/>/g, '&gt;');
50
+ }
51
+
52
+ /**
53
+ * Convert a human-readable name into a URL-safe slug.
54
+ * @param {string} str - Input string.
55
+ * @returns {string} Lower-cased, hyphenated slug.
56
+ */
57
+ function slugify(str)
58
+ {
59
+ return (str || '').toString()
60
+ .toLowerCase()
61
+ .replace(/[^a-z0-9]+/g, '-')
62
+ .replace(/(^-|-$)/g, '');
63
+ }
64
+
65
+ /**
66
+ * Format a byte count into a human-readable string (B / KB / MB / GB).
67
+ * @param {number} n - Byte count.
68
+ * @returns {string}
69
+ */
70
+ function formatBytes(n)
71
+ {
72
+ if (n === 0) return '0 B';
73
+ const k = 1024;
74
+ const sizes = ['B', 'KB', 'MB', 'GB'];
75
+ const i = Math.floor(Math.log(n) / Math.log(k));
76
+ return (n / Math.pow(k, i)).toFixed(i ? 1 : 0) + ' ' + sizes[i];
77
+ }
78
+
79
+ /* ── JSON / Code Rendering ─────────────────────────────────────────────────── */
80
+
81
+ /**
82
+ * Build a highlighted `<pre>` block containing pretty-printed JSON.
83
+ * @param {*} obj - Any JSON-serialisable value.
84
+ * @returns {string} HTML string.
85
+ */
86
+ function jsonHtml(obj)
87
+ {
88
+ return `<pre class="code language-json"><code>${escapeHtml(JSON.stringify(obj, null, 2))}</code></pre>`;
89
+ }
90
+
91
+ /**
92
+ * Render a JSON object into a container element and trigger Prism highlighting.
93
+ * @param {Element|null} container - Target element.
94
+ * @param {*} obj - JSON-serialisable value.
95
+ */
96
+ function showJsonResult(container, obj)
97
+ {
98
+ if (!container) return;
99
+ container.innerHTML = jsonHtml(obj);
100
+ try { highlightAllPre(); } catch (e) { }
101
+ }
102
+
103
+ /* ── Prism / Code-Block Helpers ────────────────────────────────────────────── */
104
+
105
+ /**
106
+ * Trigger Prism syntax highlighting on all `<pre class="code">` blocks, or
107
+ * fall back to a simple escape-and-wrap if Prism is not loaded.
108
+ */
109
+ function highlightAllPre()
110
+ {
111
+ if (window.Prism && typeof Prism.highlightAll === 'function')
112
+ {
113
+ try { Prism.highlightAll(); } catch (e) { }
114
+ document.querySelectorAll('pre.code').forEach(p => p.dataset.miniExpressHighlighted = '1');
115
+ return;
116
+ }
117
+ try
118
+ {
119
+ document.querySelectorAll('pre.code').forEach(p =>
120
+ {
121
+ if (p.dataset.miniExpressHighlighted) return;
122
+ const raw = p.textContent || p.innerText || '';
123
+ p.innerHTML = '<code>' + escapeHtml(raw) + '</code>';
124
+ p.dataset.miniExpressHighlighted = '1';
125
+ });
126
+ } catch (e) { }
127
+ }
128
+
129
+ /**
130
+ * Strip common leading whitespace from every `<pre>` block so that indented
131
+ * source pasted into the HTML renders flush-left.
132
+ */
133
+ function dedentAllPre()
134
+ {
135
+ document.querySelectorAll('pre').forEach(pre =>
136
+ {
137
+ try
138
+ {
139
+ if (pre.dataset.miniExpressDedented) return;
140
+ const txt = pre.textContent || '';
141
+ const lines = txt.replace(/\r/g, '').split('\n');
142
+
143
+ while (lines.length && lines[0].trim() === '') lines.shift();
144
+ while (lines.length && lines[lines.length - 1].trim() === '') lines.pop();
145
+ if (!lines.length) { pre.dataset.miniExpressDedented = '1'; return; }
146
+
147
+ const indents = lines.filter(l => l.trim()).map(l =>
148
+ {
149
+ const match = l.match(/^[\t ]*/)[0] || '';
150
+ return match.replace(/\t/g, ' ').length;
151
+ });
152
+ const minIndent = indents.length ? Math.min(...indents) : 0;
153
+
154
+ if (minIndent > 0)
155
+ {
156
+ const dedented = lines.map(l =>
157
+ {
158
+ const s = l.replace(/\t/g, ' ');
159
+ return s.slice(Math.min(minIndent, s.length));
160
+ }).join('\n');
161
+ pre.textContent = dedented;
162
+ }
163
+ pre.dataset.miniExpressDedented = '1';
164
+ } catch (e) { }
165
+ });
166
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * playground.js
3
+ * Echo playground forms — JSON, URL-encoded, and plain-text body parsers.
4
+ *
5
+ * Depends on: helpers.js (provides $, on, escapeHtml, showJsonResult,
6
+ * highlightAllPre)
7
+ */
8
+
9
+ /**
10
+ * Wire the three echo playground forms so submissions hit the server and
11
+ * display the response. Called once from the DOMContentLoaded handler in
12
+ * app.js.
13
+ */
14
+ function initPlayground()
15
+ {
16
+ /* JSON echo */
17
+ on($('#jsonPlay'), 'submit', async (e) =>
18
+ {
19
+ e.preventDefault();
20
+ const raw = e.target.json.value || '';
21
+ const playResult = $('#playResult');
22
+
23
+ try { JSON.parse(raw); }
24
+ catch (err)
25
+ {
26
+ playResult.innerHTML = `<pre class="code"><code>${escapeHtml('Invalid JSON: ' + err.message)}</code></pre>`;
27
+ return;
28
+ }
29
+
30
+ const r = await fetch('/echo', {
31
+ method: 'POST',
32
+ headers: { 'Content-Type': 'application/json' },
33
+ body: raw,
34
+ });
35
+ const j = await r.json();
36
+ showJsonResult(playResult, j);
37
+ });
38
+
39
+ /* URL-encoded echo */
40
+ on($('#urlPlay'), 'submit', async (e) =>
41
+ {
42
+ e.preventDefault();
43
+ const body = e.target.url.value || '';
44
+ const playResult = $('#playResult');
45
+
46
+ const r = await fetch('/echo-urlencoded', {
47
+ method: 'POST',
48
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
49
+ body,
50
+ });
51
+ const j = await r.json();
52
+ showJsonResult(playResult, j);
53
+ });
54
+
55
+ /* Plain-text echo */
56
+ on($('#textPlay'), 'submit', async (e) =>
57
+ {
58
+ e.preventDefault();
59
+ const txt = e.target.txt.value || '';
60
+ const playResult = $('#playResult');
61
+
62
+ const r = await fetch('/echo-text', {
63
+ method: 'POST',
64
+ headers: { 'Content-Type': 'text/plain' },
65
+ body: txt,
66
+ });
67
+ const text = await r.text();
68
+ playResult.innerHTML = `<pre class="code"><code>${escapeHtml(text)}</code></pre>`;
69
+ try { highlightAllPre(); } catch (e) { }
70
+ });
71
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * proxy.js
3
+ * Proxy playground — fetches an external URL through the server and renders
4
+ * the response based on its content-type (JSON, image, audio/video, text,
5
+ * or a binary download link).
6
+ *
7
+ * Depends on: helpers.js (provides $, on, escapeHtml, showJsonResult,
8
+ * highlightAllPre)
9
+ */
10
+
11
+ /**
12
+ * Wire the proxy form. Called once from the DOMContentLoaded handler in
13
+ * app.js.
14
+ */
15
+ function initProxy()
16
+ {
17
+ const proxyForm = $('#proxyForm');
18
+ const proxyResult = $('#proxyResult');
19
+ if (!proxyForm) return;
20
+
21
+ on(proxyForm, 'submit', async (e) =>
22
+ {
23
+ e.preventDefault();
24
+ const urlInput = $('#proxyUrl');
25
+ const url = urlInput && urlInput.value ? urlInput.value.trim() : '';
26
+
27
+ if (!url)
28
+ {
29
+ proxyResult.innerHTML = `<pre class="code"><code>${escapeHtml('Please enter a URL')}</code></pre>`;
30
+ return;
31
+ }
32
+
33
+ try
34
+ {
35
+ proxyResult.innerHTML = `<div class="muted">Fetching ${escapeHtml(url)}…</div>`;
36
+ const r = await fetch('/proxy?url=' + encodeURIComponent(url));
37
+
38
+ /* Upstream error */
39
+ if (r.status >= 400)
40
+ {
41
+ const j = await r.json();
42
+ showJsonResult(proxyResult, j);
43
+ return;
44
+ }
45
+
46
+ const ct = (r.headers && typeof r.headers.get === 'function')
47
+ ? (r.headers.get('content-type') || '')
48
+ : '';
49
+
50
+ /* JSON */
51
+ if (ct.includes('application/json') || ct.includes('application/problem+json'))
52
+ {
53
+ showJsonResult(proxyResult, await r.json());
54
+ }
55
+ /* Image */
56
+ else if (ct.startsWith('image/'))
57
+ {
58
+ const blob = new Blob([await r.arrayBuffer()], { type: ct });
59
+ const src = URL.createObjectURL(blob);
60
+ proxyResult.innerHTML =
61
+ `<div style="display:flex;align-items:center;gap:12px">` +
62
+ `<img src="${src}" style="max-width:240px;max-height:240px;border-radius:8px"/>` +
63
+ `<div class="mono" style="max-width:480px;overflow:auto">${escapeHtml('Image received: ' + ct)}</div></div>`;
64
+ }
65
+ /* Audio / Video / Octet-stream */
66
+ else if (ct.startsWith('audio/') || ct.startsWith('video/') || ct === 'application/octet-stream' || ct.includes('wav') || ct.includes('wave'))
67
+ {
68
+ const proxiedUrl = '/proxy?url=' + encodeURIComponent(url);
69
+ let mediaHtml = '';
70
+ if (ct.startsWith('audio/'))
71
+ mediaHtml = `<audio controls src="${proxiedUrl}" style="max-width:480px;display:block;margin-bottom:8px"></audio>`;
72
+ else if (ct.startsWith('video/'))
73
+ mediaHtml = `<video controls src="${proxiedUrl}" style="max-width:480px;display:block;margin-bottom:8px"></video>`;
74
+ proxyResult.innerHTML = `<div>${mediaHtml}<div class="mono">${escapeHtml('Streaming: ' + ct)}</div></div>`;
75
+ }
76
+ /* Text */
77
+ else if (ct.startsWith('text/') || ct === '')
78
+ {
79
+ const txt = await r.text();
80
+ proxyResult.innerHTML = `<pre class="code"><code>${escapeHtml(txt)}</code></pre>`;
81
+ try { highlightAllPre(); } catch (e) { }
82
+ }
83
+ /* Binary fallback — offer download */
84
+ else
85
+ {
86
+ const ab = await r.arrayBuffer();
87
+ const blob = new Blob([ab], { type: ct || 'application/octet-stream' });
88
+ const href = URL.createObjectURL(blob);
89
+ proxyResult.innerHTML =
90
+ `<div class="mono">${escapeHtml('Binary response: ' + ct + ' — ' + ab.byteLength + ' bytes')}</div>` +
91
+ `<div style="margin-top:8px"><a href="${href}" download="proxied-file">Download file</a></div>`;
92
+ }
93
+ } catch (err)
94
+ {
95
+ proxyResult.innerHTML = `<pre class="code"><code>${escapeHtml(String(err))}</code></pre>`;
96
+ }
97
+ });
98
+ }