Last active
April 3, 2025 22:46
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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