Skip to content

Instantly share code, notes, and snippets.

@marcogrcr
Last active April 3, 2025 22:46
Show Gist options
  • Save marcogrcr/454e3208d57f0a924161b3b33a4ac8fe to your computer and use it in GitHub Desktop.
Save marcogrcr/454e3208d57f0a924161b3b33a4ac8fe to your computer and use it in GitHub Desktop.
A simple node.js HTTP/1.1 server that echoes back the request
import { createServer } from "node:http";
export interface CreateEchoServerInput {
/**
* Indicates the echo mode.
* - `"body"` creates a response controlled by the request body with an {@link EchoBody} shape.
* - `"request"` echoes the request headers and body.
* - `"request-in-body"` echo the request in a JSON object in the response body.
* @default "request"
*/
readonly echo?: "body" | "request" | "request-in-body";
/**
* Indicates whether to use `console.log()` to log the received request.
* @default false
*/
readonly log?: boolean;
/**
* Indicates the port to listen to requests.
* @default 8080
*/
readonly port?: number;
/**
* Indicates the status code to use by default.
* @default 200
*/
readonly status?: number;
}
export interface CreateEchoServerOutput {
/** The base URL to use for sending requests. */
readonly baseUrl: URL;
/** Closes the echo server. */
close(): Promise<void>;
}
/** The body of the request when {@link CreateEchoServerInput["echo"]} is set to `"body"`. */
export type EchoBody =
| {
/** Indicates whether to abort the request by closing the connection before returning a response. */
readonly abort: true;
}
| {
readonly abort?: false;
/**
* The HTTP status code to return.
* Defaults to {@link CreateEchoServerInput["status"]}.
*/
readonly status?: number | null;
/** The HTTP headers to return. */
readonly headers?: Record<string, string | readonly string[]> | null;
/** The HTTP body to return. */
readonly body?: unknown;
};
/** Creates an HTTP server that echoes HTTP requests. */
export async function createEchoServer(
input: CreateEchoServerInput = {},
): Promise<CreateEchoServerOutput> {
const { echo = "request", log = false, port = 8080, status = 200 } = input;
const server = createServer(async (req, res) => {
const { method, url: path, headersDistinct: headers, rawHeaders } = req;
const body = await new Promise<string | null>((resolve) => {
const buffer: unknown[] = [];
req.on("data", (chunk) => buffer.push(chunk));
req.on("end", () => resolve(buffer.length ? buffer.join("") : null));
});
if (log) {
console.log(`${method} ${path}`);
for (let i = 0; i < rawHeaders.length; i += 2) {
console.log(`${rawHeaders[i]}: ${rawHeaders[i + 1]}`);
}
console.log("");
if (body) {
console.log(body);
}
}
switch (echo) {
case "body": {
try {
const parsedBody = JSON.parse(body || "{}") as EchoBody;
if (parsedBody.abort) {
// close underlying TCP connection and stop processing
res.destroy();
return;
}
const {
status: resStatus,
headers: resHeaders,
body: resBody,
} = parsedBody;
res.statusCode = resStatus ?? status;
Object.entries(resHeaders ?? {}).forEach(([key, value]) =>
res.appendHeader(key, (value ?? "") as string | string[]),
);
res.write(
typeof resBody === "object" && resBody
? JSON.stringify(resBody)
: (resBody?.toString() ?? ""),
);
} catch {
res.statusCode = 500;
res.setHeader("content-type", "application/json");
res.write(
JSON.stringify({ message: "Failed to parse body as JSON", body }),
);
}
break;
}
case "request-in-body": {
let resBody;
try {
resBody = body ? (JSON.parse(body) ?? "null") : undefined;
} catch {
resBody = body;
}
res.statusCode = status;
res.setHeader("content-type", "application/json");
res.write(
JSON.stringify({
method,
path,
headers,
body: resBody,
}),
);
break;
}
case "request": {
res.statusCode = status;
Object.entries(headers).forEach(([key, value]) =>
res.appendHeader(key, value ?? ""),
);
if (body) {
res.write(body);
}
break;
}
}
res.end();
});
server.listen(port);
await new Promise<void>((resolve, reject) => {
server.on("listening", () => resolve());
server.on("error", (e) => reject(e));
});
return {
baseUrl: new URL(`http://localhost:${port}`),
close: async () =>
await new Promise<void>((resolve, reject) =>
server.close((e) => (e ? reject(e) : resolve())),
),
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment