diff --git a/.changeset/purple-crews-shake.md b/.changeset/purple-crews-shake.md new file mode 100644 index 0000000..f91d867 --- /dev/null +++ b/.changeset/purple-crews-shake.md @@ -0,0 +1,5 @@ +--- +"@nornir/rest": patch +--- + +Handle bad content types and invalid payloads diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1587e7f..3f3bb32 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,6 +9,7 @@ concurrency: cancel-in-progress: true env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + GH_TOKEN: ${{ secrets.GH_TOKEN }} jobs: unit-tests: name: Run unit tests for this service diff --git a/packages/rest/__tests__/src/parse.spec.mts b/packages/rest/__tests__/src/parse.spec.mts new file mode 100644 index 0000000..73c77b1 --- /dev/null +++ b/packages/rest/__tests__/src/parse.spec.mts @@ -0,0 +1,19 @@ +import {httpEventParser, NornirParseError, UnparsedHttpEvent} from "../../dist/runtime/index.mjs"; + +describe("Parsing", () => { + it("Should throw correct error on failure to parse", () => { + const parser = httpEventParser() + + const event: UnparsedHttpEvent = { + headers: { + "content-type": "application/json" + }, + rawBody: Buffer.from("not json"), + method: "GET", + path: "/", + rawQuery: "" + } + + expect(() => parser(event)).toThrow(NornirParseError) + }) +}) diff --git a/packages/rest/src/runtime/error.mts b/packages/rest/src/runtime/error.mts index e5a3b50..e57140a 100644 --- a/packages/rest/src/runtime/error.mts +++ b/packages/rest/src/runtime/error.mts @@ -17,7 +17,7 @@ export abstract class NornirRestError extends Error implements NodeJS.ErrnoExcep /** * Error type for exceptions that require information about the request. */ -export abstract class NornirRestRequestError extends NornirRestError { +export abstract class NornirRestRequestError extends NornirRestError { constructor( public readonly request: Request, message: string @@ -28,6 +28,7 @@ export abstract class NornirRestRequestError extend abstract toHttpResponse(registry: AttachmentRegistry): HttpResponse | Promise; } + interface ErrorMapping { errorMatch(error: unknown): boolean; toHttpResponse(error: unknown, registry: AttachmentRegistry): HttpResponse | Promise; diff --git a/packages/rest/src/runtime/index.mts b/packages/rest/src/runtime/index.mts index bbe738d..9e13937 100644 --- a/packages/rest/src/runtime/index.mts +++ b/packages/rest/src/runtime/index.mts @@ -12,7 +12,7 @@ export { export {RouteHolder, NornirRestRequestValidationError} from './route-holder.mjs' export {NornirRestRequestError, NornirRestError, httpErrorHandler, mapError, mapErrorClass} from './error.mjs' export {ApiGatewayProxyV2, startLocalServer} from "./converters.mjs" -export {httpEventParser, HttpBodyParser, HttpBodyParserMap, HttpQueryStringParser} from "./parse.mjs" +export {httpEventParser, HttpBodyParser, HttpBodyParserMap, HttpQueryStringParser, NornirParseError} from "./parse.mjs" export {httpResponseSerializer, HttpBodySerializer, HttpBodySerializerMap} from "./serialize.mjs" export {normalizeEventHeaders, normalizeHeaders, getContentType} from "./utils.mjs" export {Router} from "./router.mjs" diff --git a/packages/rest/src/runtime/parse.mts b/packages/rest/src/runtime/parse.mts index 2e086d7..992489a 100644 --- a/packages/rest/src/runtime/parse.mts +++ b/packages/rest/src/runtime/parse.mts @@ -1,12 +1,33 @@ -import {HttpEvent, MimeType, UnparsedHttpEvent} from "./http-event.mjs"; +import {HttpEvent, HttpResponse, HttpStatusCode, MimeType, UnparsedHttpEvent} from "./http-event.mjs"; import querystring from "node:querystring"; import {getContentType} from "./utils.mjs"; +import {NornirRestError} from "./error.mjs"; +import {AttachmentRegistry} from "@nornir/core"; export type HttpBodyParser = (body: Buffer) => unknown export type HttpBodyParserMap = Partial> +export class NornirParseError extends NornirRestError { + constructor(cause: Error) { + super("Failed to parse request. Bad content-type or invalid body", cause) + this.cause = cause; + } + + public toHttpResponse(): HttpResponse { + return { + statusCode: HttpStatusCode.UnprocessableEntity, + headers: { + "content-type": MimeType.TextPlain, + }, + body: { + message: this.message, + } + } + } +} + export type HttpQueryStringParser = (query: string) => HttpEvent["query"] const DEFAULT_BODY_PARSERS: HttpBodyParserMap & {"default": HttpBodyParser} = { "application/json": (body) => JSON.parse(body.toString("utf8")), @@ -21,12 +42,16 @@ export function httpEventParser(bodyParserMap?: HttpBodyParserMap, queryStringPa ...bodyParserMap }; return (event: UnparsedHttpEvent): HttpEvent => { - const contentType = getContentType(event.headers) || "default"; - const bodyParser = bodyParsers[contentType] || bodyParsers["default"]; - return { - ...event, - body: bodyParser(event.rawBody), - query: queryStringParser(event.rawQuery), + try { + const contentType = getContentType(event.headers) || "default"; + const bodyParser = bodyParsers[contentType] || bodyParsers["default"]; + return { + ...event, + body: bodyParser(event.rawBody), + query: queryStringParser(event.rawQuery), + } + } catch (error) { + throw new NornirParseError(error as Error) } } } diff --git a/packages/rest/src/runtime/router.mts b/packages/rest/src/runtime/router.mts index 0164499..2be596c 100644 --- a/packages/rest/src/runtime/router.mts +++ b/packages/rest/src/runtime/router.mts @@ -99,3 +99,4 @@ export class NornirRouteNotFoundError extends NornirRestRequestError