108 lines
2.6 KiB
TypeScript
108 lines
2.6 KiB
TypeScript
import { IncomingMessage, ServerResponse } from 'http';
|
|
|
|
export interface RequestContext {
|
|
method: string;
|
|
url: URL;
|
|
pathname: string;
|
|
headers: IncomingMessage['headers'];
|
|
remoteAddress: string;
|
|
bearerToken: string | null;
|
|
}
|
|
|
|
const MAX_BODY_BYTES = 1 * 1024 * 1024; // 1 MiB
|
|
|
|
export function getBearerToken(headers: IncomingMessage['headers']): string | null {
|
|
const raw = headers.authorization;
|
|
|
|
if (typeof raw !== 'string') {
|
|
return null;
|
|
}
|
|
|
|
const trimmed = raw.trim();
|
|
|
|
if (!/^bearer\s+/iu.test(trimmed)) {
|
|
return null;
|
|
}
|
|
|
|
const token = trimmed.replace(/^bearer\s+/iu, '').trim();
|
|
|
|
return token.length > 0 ? token : null;
|
|
}
|
|
|
|
export async function readJsonBody<T>(req: IncomingMessage): Promise<T> {
|
|
const length = Number(req.headers['content-length'] ?? 0);
|
|
|
|
if (length > MAX_BODY_BYTES) {
|
|
throw new HttpError(413, 'Request body too large', 'BODY_TOO_LARGE');
|
|
}
|
|
|
|
const chunks: Buffer[] = [];
|
|
let received = 0;
|
|
|
|
for await (const chunk of req) {
|
|
const buffer = chunk instanceof Buffer ? chunk : Buffer.from(chunk as string);
|
|
|
|
received += buffer.length;
|
|
|
|
if (received > MAX_BODY_BYTES) {
|
|
throw new HttpError(413, 'Request body too large', 'BODY_TOO_LARGE');
|
|
}
|
|
|
|
chunks.push(buffer);
|
|
}
|
|
|
|
if (chunks.length === 0) {
|
|
return {} as T;
|
|
}
|
|
|
|
const raw = Buffer.concat(chunks).toString('utf8');
|
|
|
|
try {
|
|
return JSON.parse(raw) as T;
|
|
} catch {
|
|
throw new HttpError(400, 'Invalid JSON body', 'INVALID_JSON');
|
|
}
|
|
}
|
|
|
|
export function sendJson(res: ServerResponse, status: number, payload: unknown): void {
|
|
if (!res.headersSent) {
|
|
res.statusCode = status;
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.setHeader('Cache-Control', 'no-store');
|
|
}
|
|
|
|
res.end(JSON.stringify(payload));
|
|
}
|
|
|
|
export function sendText(res: ServerResponse, status: number, text: string, contentType = 'text/plain; charset=utf-8'): void {
|
|
if (!res.headersSent) {
|
|
res.statusCode = status;
|
|
res.setHeader('Content-Type', contentType);
|
|
res.setHeader('Cache-Control', 'no-store');
|
|
}
|
|
|
|
res.end(text);
|
|
}
|
|
|
|
export class HttpError extends Error {
|
|
readonly status: number;
|
|
readonly code: string;
|
|
|
|
constructor(status: number, message: string, code: string) {
|
|
super(message);
|
|
this.status = status;
|
|
this.code = code;
|
|
}
|
|
}
|
|
|
|
export function sendError(res: ServerResponse, error: unknown): void {
|
|
if (error instanceof HttpError) {
|
|
sendJson(res, error.status, { error: error.message, errorCode: error.code });
|
|
return;
|
|
}
|
|
|
|
const message = error instanceof Error ? error.message : 'Internal server error';
|
|
|
|
sendJson(res, 500, { error: message, errorCode: 'INTERNAL_ERROR' });
|
|
}
|