/**
* Minimal HTTP client using only Node.js built-ins.
*
* Exports:
* - HttpClient: class with methods get/post/put/patch/delete/head/options/custom
* - HttpResponse: response wrapper (left unchanged per request)
*
* HttpResponse API:
* - statusCode: number
* - statusText: string
* - ok: boolean (2xx)
* - headers: object
* - text(): Promise<string>
* - isJson(): Promise<boolean>
* - json(): Promise<any>
*
* HttpClient usage (example):
* const { HttpClient } = require('./client');
* const client = new HttpClient();
* const res = await client.get('http://localhost:3000/auth', {
* headers: { 'content-type':'application/json' },
* // body: ..., // .post/.put/.patch only
* });
* if (!res.ok) {
* const txt = await res.text();
* return txt;
* }
* if (!await res.isJson()) {
* const txt = await res.text();
* return txt;
* }
* const obj = await res.json();
* // ... whatever with obj ...
*/
const { request } = require("http");
const { URL } = require("url");
class HttpResponse {
/**
* Wrap an IncomingMessage.
* @param {import('http').IncomingMessage} incoming
*/
constructor(incoming) {
this._incoming = incoming;
this._buf = null;
this.statusCode = incoming.statusCode || 0;
this.statusText = incoming.statusMessage || "";
this.ok = this.statusCode >= 200 && this.statusCode < 300;
this.headers = incoming.headers;
this.header = incoming.header;
this._json = null;
}
/**
* Buffer the full response body (private).
* @returns {Promise<Buffer>}
*/
async #readAll() {
return new Promise((resolve, reject) => {
const bufs = [];
this._incoming.on("data", (b) => bufs.push(b));
this._incoming.on("end", () => resolve(Buffer.concat(bufs)));
this._incoming.on("error", reject);
});
}
/**
* Return the response body as a string. Buffers on first call and reuses the value.
* @returns {Promise<string>}
*/
async text() {
if (!this._buf) this._buf = await this.#readAll();
return this._buf.toString();
}
/**
* Feature-detect whether the body is valid JSON.
* Uses text() as the single source-of-truth for the buffered body.
* @returns {Promise<boolean>}
*/
async isJson() {
try {
this._json = JSON.parse(await this.text());
return true;
} catch {
return false;
}
}
/**
* Parse and return JSON body. Throws if body is not JSON.
* @returns {Promise<any>}
*/
async json() {
if ((await this.isJson()) === false) throw new Error("not json");
if (this._json == null) this._json = JSON.parse(await this.text());
return this._json;
}
}
class HttpClient {
/**
* Internal request factory that performs a request and returns HttpResponse.
* Private by design: use convenience methods (get/post/...) instead.
*
* @param {string} method HTTP method
* @param {string} url Request URL
* @param {{ headers?: Record<string,string>, body?: string|Buffer|any }} [config]
* @returns {Promise<HttpResponse>}
*/
async #requestFactory(method, url, config = {}) {
const u = new URL(url);
const req = request({
method,
protocol: u.protocol,
hostname: u.hostname,
port: u.port,
path: u.pathname + u.search,
headers: config.headers || {},
});
// write body if provided
if (config.body) {
if (typeof config.body === "string" || Buffer.isBuffer(config.body)) {
req.write(config.body);
} else {
req.write(JSON.stringify(config.body));
}
}
req.end();
const incoming = await new Promise((resolve, reject) => {
req.on("error", reject);
req.on("response", resolve);
});
return new HttpResponse(incoming);
}
// Convenience methods for common HTTP verbs:
/**
* GET request
* @param {string} url
* @param {{ headers?: Record<string,string>, body?: string|Buffer|any }} [config]
*/
get(url, config) {
return this.#requestFactory("GET", url, config);
}
/**
* POST request
*/
post(url, config) {
return this.#requestFactory("POST", url, config);
}
/**
* PUT request
*/
put(url, config) {
return this.#requestFactory("PUT", url, config);
}
/**
* PATCH request
*/
patch(url, config) {
return this.#requestFactory("PATCH", url, config);
}
/**
* DELETE request
*/
delete(url, config) {
return this.#requestFactory("DELETE", url, config);
}
/**
* HEAD request
*/
head(url, config) {
return this.#requestFactory("HEAD", url, config);
}
/**
* OPTIONS request
*/
options(url, config) {
return this.#requestFactory("OPTIONS", url, config);
}
/**
* Custom method
* @param {string} method
*/
custom(method, url, config) {
return this.#requestFactory(method, url, config);
}
}
module.exports = { HttpClient, HttpResponse };