← back to Robet Site
round2 item (7): server-side robustness — try/catch on contact+keys writes (graceful 500), global JSON error handler (honors status), process-level guards; +malformed-body test (8/8)
4b3c1142224e2f77e4a957b75d6bb416ca3c0676 · 2026-06-29 21:38:37 -0700 · Steve
Files touched
M server.jsM tests/api.test.mjs
Diff
commit 4b3c1142224e2f77e4a957b75d6bb416ca3c0676
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Jun 29 21:38:37 2026 -0700
round2 item (7): server-side robustness — try/catch on contact+keys writes (graceful 500), global JSON error handler (honors status), process-level guards; +malformed-body test (8/8)
---
server.js | 25 +++++++++++++++++++++----
tests/api.test.mjs | 10 ++++++++++
2 files changed, 31 insertions(+), 4 deletions(-)
diff --git a/server.js b/server.js
index 7bf21d0..cabd5d4 100644
--- a/server.js
+++ b/server.js
@@ -102,8 +102,13 @@ app.post('/api/contact', (req, res) => {
const m = String(message).slice(0, 5000).trim();
if (!EMAIL_RE.test(e)) return res.status(400).json({ ok: false, error: 'A valid email is required.' });
if (!m) return res.status(400).json({ ok: false, error: 'A message is required.' });
- fs.mkdirSync(path.dirname(CONTACTS_FILE), { recursive: true });
- fs.appendFileSync(CONTACTS_FILE, JSON.stringify({ name: n, email: e, message: m, ip: clientIp(req), at: new Date().toISOString() }) + '\n');
+ try {
+ fs.mkdirSync(path.dirname(CONTACTS_FILE), { recursive: true });
+ fs.appendFileSync(CONTACTS_FILE, JSON.stringify({ name: n, email: e, message: m, ip: clientIp(req), at: new Date().toISOString() }) + '\n');
+ } catch (err) {
+ console.error('contact write failed:', err.message);
+ return res.status(500).json({ ok: false, error: 'Could not save your message — please try again.' });
+ }
res.json({ ok: true });
});
@@ -121,14 +126,14 @@ app.post('/api/keys', adminAuth, (req, res) => {
if (!name || !value) return res.status(400).json({ ok: false, error: 'name and value required' });
const keys = readJson(KEYS_FILE, {});
keys[name] = { value, updated_at: new Date().toISOString() };
- writeJson(KEYS_FILE, keys);
+ try { writeJson(KEYS_FILE, keys); } catch (err) { console.error('keys write failed:', err.message); return res.status(500).json({ ok: false, error: 'Could not save the key.' }); }
res.json({ ok: true, name, masked: last4(value) });
});
app.delete('/api/keys/:name', adminAuth, (req, res) => {
const keys = readJson(KEYS_FILE, {});
delete keys[req.params.name];
- writeJson(KEYS_FILE, keys);
+ try { writeJson(KEYS_FILE, keys); } catch (err) { console.error('keys write failed:', err.message); return res.status(500).json({ ok: false, error: 'Could not delete the key.' }); }
res.json({ ok: true });
});
@@ -142,4 +147,16 @@ app.use((req, res) => {
res.status(404).json({ ok: false, error: 'Not found' });
});
+// ── global error handler: always clean JSON, never an HTML stack trace ──
+app.use((err, req, res, _next) => {
+ const code = err.status || err.statusCode || 500;
+ console.error('route error:', code, err && err.message);
+ if (res.headersSent) return;
+ res.status(code).json({ ok: false, error: code === 400 ? 'Bad request' : 'Server error' });
+});
+
+// keep the process alive on unexpected async faults (log, don't crash the booking site)
+process.on('unhandledRejection', (r) => console.error('unhandledRejection:', r));
+process.on('uncaughtException', (e) => console.error('uncaughtException:', e && e.message));
+
app.listen(PORT, () => console.log(`robet-site → http://127.0.0.1:${PORT}`));
diff --git a/tests/api.test.mjs b/tests/api.test.mjs
index 5b07318..0064db3 100644
--- a/tests/api.test.mjs
+++ b/tests/api.test.mjs
@@ -85,6 +85,16 @@ test('contact: honeypot + validation + rate limit', async () => {
assert.ok(saw429, 'rate limit should trigger 429 on burst');
});
+test('malformed JSON body → clean JSON error, not HTML stack', async () => {
+ const r = await fetch(`${BASE}/api/contact`, {
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{bad json',
+ });
+ assert.equal(r.status, 400);
+ assert.match(r.headers.get('content-type') || '', /application\/json/);
+ const j = await r.json();
+ assert.equal(j.ok, false);
+});
+
test('admin auth gates + key masking', async () => {
assert.equal((await fetch(`${BASE}/admin`)).status, 401); // no creds
assert.equal((await fetch(`${BASE}/admin`, { headers: { Authorization: ADMIN } })).status, 200);
← 6e320db round-2 DTD backlog committed (8 deeper-polish items, correc
·
back to Robet Site
·
loop ledger: round2 item (7) done + deployed 4b3333d →