UNPKG

http-micro

Version:

Micro-framework on top of node's http module

194 lines (172 loc) 7.44 kB
import * as http from "http"; import * as rawBody from "raw-body"; import * as qs from "querystring"; import * as typeis from "type-is"; import * as contentType from "content-type"; import * as mimeTypes from "mime-types"; import * as httpError from "http-errors"; import { intoHttpError } from "./error-utils"; export type ParserCallback = (error: rawBody.RawBodyError, body?: string | Buffer) => void; export type Parser = (req: http.IncomingMessage, callback: ParserCallback, opts? : any) => void; const defaultLimit = 1024 * 1024 / 2; // 512Kb export function rawBodyParserFactory() { return function rawParser(req: http.IncomingMessage, callback: ParserCallback, opts?: any) { if (handleRequestBodyAbsence(req, callback)) return; let limit: number, contentLength: number, encoding: rawBody.Encoding; if (opts) { limit = opts.limit; contentLength = opts.length; encoding = opts.encoding; } limit = limit || defaultLimit; contentLength = contentLength || Number(req.headers["content-length"]); if (encoding === undefined) { let contentTypeHeader = req.headers["content-type"] as string; // Ensure that further attempts are skipped, as contentType // will throw on invalid header. Since rawParser could // potentially be passed on it's own to get a buffer back. if (contentTypeHeader) { try { let ct = contentType.parse(contentTypeHeader); encoding = ct.parameters["charset"] as any; if (!encoding) { // No valid encoding was found, but content-type // header is valid. So, pick up the default // encoding for the mime. let mimeCharset = mimeTypes.charset(ct.type); encoding = mimeCharset ? mimeCharset : undefined; } } catch (err) { throw intoHttpError(err, 400); } } } if (encoding === undefined && opts) encoding = opts.defaultEncoding; rawBody(req, { limit: limit, length: contentLength, encoding, }, callback); }; } export type JsonBodyParserOpts = rawBody.Options & { reviver?: (key: any, value: any) => any }; export function jsonBodyParserFactory(opts: JsonBodyParserOpts, baseParser?: Parser) { let rawParser = baseParser || rawBodyParserFactory(); let reviver = opts ? opts.reviver : undefined; return function jsonParser(req: http.IncomingMessage, callback: ParserCallback, baseParserOpts?: any) { rawParser(req, function (err, body) { if (err) { return callback(err); } let res; try { if (typeof body !== "string") throw new Error("buffered raw body is not a string to parse as json"); res = JSON.parse(body, reviver); } catch (e) { return callback(e); } callback(null, res); }, baseParserOpts); }; } export type FormBodyParserOpts = rawBody.Options & { parser?: (str: string, sep?: string, eq?: string, options?: qs.ParseOptions) => any; sep?: string; eq?: string; options?: qs.ParseOptions; }; export function formBodyParserFactory(opts: FormBodyParserOpts, baseParser?: Parser) { let rawParser = baseParser || rawBodyParserFactory(); let qsParse: any, sep: string, eq: string, options: qs.ParseOptions; if (opts) { qsParse = opts.parser; sep = opts.sep; eq = opts.eq; options = opts.options; } qsParse = qsParse || qs.parse; return function formBodyParser(req: http.IncomingMessage, callback: ParserCallback, baseParserOpts?: any) { let baseOpts = baseParserOpts; // TODO: Not very happy with the implementation here, for passing override opts to // the raw parser. It works, however, could be better designed. if (baseOpts == null || baseOpts.defaultEncoding === undefined) { // It's important to pass in the default encoding as true (translates to 'utf-8'), // since, mime-types don't resolve the default charset for // `application/x-www-form-urlencoded` // TODO: Default charset Latin-1? baseOpts = Object.assign({}, baseOpts, { defaultEncoding: true }); } rawParser(req, function (err, body) { if (err) { return callback(err); } let res; try { if (typeof body !== "string") throw new Error("buffered raw body is not a string to parse as url encoded form"); // TODO: How to handle charset? res = qsParse(body, sep, eq, options); } catch (e) { return callback(e); } callback(null, res); }, baseOpts); }; } export type AnyParserOptions = FormBodyParserOpts & JsonBodyParserOpts; export function anyBodyParserFactory(opts?: AnyParserOptions, baseParser?: Parser) { let rawParser = baseParser || rawBodyParserFactory(); let jsonParser = jsonBodyParserFactory(opts, rawParser); let formParser = formBodyParserFactory(opts, rawParser); let types = ["json", "urlencoded"]; return function anyBodyParser(req: http.IncomingMessage, callback: ParserCallback, baseParserOpts?: any) { if (handleRequestBodyAbsence(req, callback)) return; let t = typeis(req, types); switch (t) { case types[0]: { jsonParser(req, callback, baseParserOpts); break; } case types[1]: { formParser(req, callback, baseParserOpts); break; } default: { rawParser(req, callback, baseParserOpts); } } }; } export function createAsyncParser(parser: Parser) { return function parse(req: http.IncomingMessage, opts?: any): Promise<any> { return new Promise((resolve, reject) => { parser(req, (err, body) => { if (err) { reject(intoHttpError(err, 400)); } else { resolve(body); } }, opts); }); } } export function parseBody<T>(req: http.IncomingMessage, opts?: any, parser: Parser = anyBodyParserFactory()) { let finalParser = createAsyncParser(parser); return finalParser(req, opts) as Promise<T>; } export function handleRequestBodyAbsence(req: http.IncomingMessage, callback: ParserCallback) { if (!hasBody(req)) { callback(null, null); return true; } return false; } export function hasBody(req: http.IncomingMessage) { let headers = req.headers; if (headers["transfer-encoding"] !== undefined) return true; let contentLength = headers["content-length"]; if (contentLength && Number(contentLength) > 0) return true; return false; }