Skip to content

Commit

Permalink
Merge pull request #14 from jfrconley/joco/handle-bad-content-type
Browse files Browse the repository at this point in the history
handle invalid payloads and content types
  • Loading branch information
jfrconley authored Dec 1, 2023
2 parents a962039 + df1d865 commit 45c6dbf
Show file tree
Hide file tree
Showing 7 changed files with 61 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .changeset/purple-crews-shake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nornir/rest": patch
---

Handle bad content types and invalid payloads
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions packages/rest/__tests__/src/parse.spec.mts
Original file line number Diff line number Diff line change
@@ -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)
})
})
3 changes: 2 additions & 1 deletion packages/rest/src/runtime/error.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Request extends HttpRequest> extends NornirRestError {
export abstract class NornirRestRequestError<Request extends HttpRequest = HttpRequest> extends NornirRestError {
constructor(
public readonly request: Request,
message: string
Expand All @@ -28,6 +28,7 @@ export abstract class NornirRestRequestError<Request extends HttpRequest> extend
abstract toHttpResponse(registry: AttachmentRegistry): HttpResponse | Promise<HttpResponse>;
}


interface ErrorMapping {
errorMatch(error: unknown): boolean;
toHttpResponse(error: unknown, registry: AttachmentRegistry): HttpResponse | Promise<HttpResponse>;
Expand Down
2 changes: 1 addition & 1 deletion packages/rest/src/runtime/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
39 changes: 32 additions & 7 deletions packages/rest/src/runtime/parse.mts
Original file line number Diff line number Diff line change
@@ -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<Record<MimeType | "default", HttpBodyParser>>

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")),
Expand All @@ -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)
}
}
}
1 change: 1 addition & 0 deletions packages/rest/src/runtime/router.mts
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,4 @@ export class NornirRouteNotFoundError extends NornirRestRequestError<HttpRequest
}
}
}

0 comments on commit 45c6dbf

Please sign in to comment.