HTTP server
This code snippets is a simple HTTP server that works with Promises. It has no external dependencies.
Note: this server does not defend against Denial Of Service attacks such as slowloris or extremely large payload data.
Usage
main.ts
import { server } from './server';
server(async ({ path, headers, getBody, method, searchParams }) => {
return {
status: 204,
statusText: 'No content',
body: 'Successful deployment',
};
});
Source code
server.ts
import http from 'http';
const PORT = process.env.PORT || 5000;
export type Handler = (
parameters: HandlerParameters
) => HandlerResponse | Promise<HandlerResponse>;
export interface HandlerParameters {
method: string;
path: string;
searchParams: URLSearchParams;
headers: http.IncomingHttpHeaders;
getBody: (characterLimit: number | undefined) => Promise<string>;
}
export interface HandlerResponse {
status: number;
statusText?: string | undefined;
body?: string | Record<string, any> | undefined;
}
/**
* This function provides a simple HTTP server.
*
* The handler function is called for each HTTP request. If the handler function returns a string in the
* body, it is sent to the client as-is. If the handler function returns an object, the server will
* convert it to JSON first.
*
* The port of the server is controlled by the environment variable `PORT`. If that variable is not specified, the server
* runs on port 5000.
*
* @param handler The function to be called on each request.
* @param logRequests Set this to `false` to prevent the server from logging each request to the console. Default: `true`
*/
export function server(handler: Handler, logRequests = true) {
const server = http.createServer(async (req, res) => {
let url: URL | undefined;
try {
url = new URL(req.url!, 'http://localhost');
const handlerParameters: HandlerParameters = {
method: req.method!,
path: url.pathname,
searchParams: url.searchParams,
headers: req.headers,
getBody: (characterLimit) => getBody(req, characterLimit),
};
const response = await handler(handlerParameters);
if (logRequests) {
console.log(response.status, req.method, url.pathname);
}
const isString = typeof response.body === 'string';
res.writeHead(response.status, response.statusText, {
'content-type': isString ? 'text/plain' : 'application/json',
});
res.write(
isString ? response.body : JSON.stringify(response.body, undefined, 2)
);
res.end();
} catch (e) {
if (logRequests) {
console.log(500, req.method, url?.pathname);
}
console.error(e);
res.writeHead(500, 'Internal server error');
res.write('Internal server error');
res.end();
}
});
const signalHandler: NodeJS.SignalsListener = (signal) => {
console.log(`Received ${signal}, closing the server process...`);
server.close(() => process.exit(0));
};
process.on('SIGTERM', signalHandler);
process.on('SIGINT', signalHandler);
server.on('error', (e: Error) => {
console.error(e);
process.exit(1);
});
server.listen(PORT, () => {
console.log(`Listening on port ${PORT}`);
});
}
function getBody(request: http.IncomingMessage, characterLimit = 1_000_000) {
return new Promise<string>((resolve) => {
let body = '';
const onData = (chunk: string) => {
body += chunk;
if (body.length > characterLimit) {
request.off('data', onData);
reject();
}
};
request.setEncoding('utf8');
request
.on('data', onData)
.on('error', () => reject())
.on('end', () => resolve(body));
});
}