feat: add HTTP response and parsing helpers for form-based UX flows
This commit is contained in:
103
src/lib/http.js
Normal file
103
src/lib/http.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
function json(status, data, extraHeaders) {
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
...(extraHeaders || {}),
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function html(status, markup, extraHeaders) {
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
headers: {
|
||||||
|
"content-type": "text/html; charset=utf-8",
|
||||||
|
...(extraHeaders || {}),
|
||||||
|
},
|
||||||
|
body: markup,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function text(status, body, contentType, extraHeaders) {
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
headers: {
|
||||||
|
"content-type": contentType || "text/plain; charset=utf-8",
|
||||||
|
...(extraHeaders || {}),
|
||||||
|
},
|
||||||
|
body,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirect(location, extraHeaders) {
|
||||||
|
return {
|
||||||
|
status: 303,
|
||||||
|
headers: {
|
||||||
|
location,
|
||||||
|
...(extraHeaders || {}),
|
||||||
|
},
|
||||||
|
body: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJSON(rawBody) {
|
||||||
|
if (!rawBody) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(rawBody);
|
||||||
|
} catch {
|
||||||
|
throw new Error("invalid_json");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFormUrlEncoded(rawBody) {
|
||||||
|
if (!rawBody) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(rawBody)
|
||||||
|
.split("&")
|
||||||
|
.filter(Boolean)
|
||||||
|
.reduce((acc, part) => {
|
||||||
|
const idx = part.indexOf("=");
|
||||||
|
if (idx === -1) {
|
||||||
|
const key = decodeURIComponent(part.replaceAll("+", " "));
|
||||||
|
acc[key] = "";
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = decodeURIComponent(part.slice(0, idx).replaceAll("+", " "));
|
||||||
|
const value = decodeURIComponent(part.slice(idx + 1).replaceAll("+", " "));
|
||||||
|
acc[key] = value;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function withQuery(path, queryObj) {
|
||||||
|
const entries = Object.entries(queryObj || {}).filter(([, v]) => v !== undefined && v !== null && v !== "");
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = entries
|
||||||
|
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
|
||||||
|
.join("&");
|
||||||
|
|
||||||
|
return `${path}?${query}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
json,
|
||||||
|
html,
|
||||||
|
text,
|
||||||
|
redirect,
|
||||||
|
parseJSON,
|
||||||
|
parseFormUrlEncoded,
|
||||||
|
withQuery,
|
||||||
|
};
|
||||||
31
test/http.test.js
Normal file
31
test/http.test.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert/strict");
|
||||||
|
const {
|
||||||
|
redirect,
|
||||||
|
parseJSON,
|
||||||
|
parseFormUrlEncoded,
|
||||||
|
withQuery,
|
||||||
|
} = require("../src/lib/http");
|
||||||
|
|
||||||
|
test("redirect returns 303 and location", () => {
|
||||||
|
const response = redirect("/app");
|
||||||
|
assert.equal(response.status, 303);
|
||||||
|
assert.equal(response.headers.location, "/app");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parseJSON throws on invalid payload", () => {
|
||||||
|
assert.throws(() => parseJSON("{invalid}"), /invalid_json/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parseFormUrlEncoded parses plus and percent encoding", () => {
|
||||||
|
const body = "title=Hello+World&body=Line%201%0ALine%202";
|
||||||
|
const parsed = parseFormUrlEncoded(body);
|
||||||
|
assert.equal(parsed.title, "Hello World");
|
||||||
|
assert.equal(parsed.body, "Line 1\nLine 2");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("withQuery appends query params", () => {
|
||||||
|
assert.equal(withQuery("/login", { returnTo: "/audio/1", flash: "done" }), "/login?returnTo=%2Faudio%2F1&flash=done");
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user