Skip to content

Commit ff599e8

Browse files
committed
feat(middlewares): use onError
This changes the validateInput() middleware to throw an error instead of returning the response. This allows for the corsOPTIONS() middleware to hook into the error response, which is now handled by a generic middleware which turns errors into Problem Detail responses using aProblem() BREAKING CHANGE: this changes the way validateInput() works
1 parent 1c0a251 commit ff599e8

File tree

3 files changed

+86
-30
lines changed

3 files changed

+86
-30
lines changed

src/corsOPTIONS.ts

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,37 @@ import type {
22
APIGatewayProxyEventV2,
33
APIGatewayProxyStructuredResultV2,
44
} from 'aws-lambda'
5-
import type { MiddlewareObj } from '@middy/core'
5+
import type { MiddlewareObj, Request } from '@middy/core'
66
import { corsHeaders } from './corsHeaders.js'
77

88
export const corsOPTIONS = (
99
...allowedMethods: string[]
10-
): MiddlewareObj<
11-
APIGatewayProxyEventV2,
12-
APIGatewayProxyStructuredResultV2
13-
> => ({
14-
before: async (req) => {
15-
if (req.event.requestContext.http.method === 'OPTIONS') {
16-
return {
17-
statusCode: 200,
18-
headers: corsHeaders(req.event, allowedMethods),
19-
}
10+
): MiddlewareObj<APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2> => {
11+
const setCorsHeaders = async (req: Request) => {
12+
if (req.response === null) {
13+
console.error(`[corsOPTIONS]`, `Response is null`)
14+
return
2015
}
21-
return undefined
22-
},
23-
after: async (req) => {
24-
if (req.response === null) return
2516
req.response = {
2617
...req.response,
2718
headers: {
2819
...req.response.headers,
2920
...corsHeaders(req.event, allowedMethods),
3021
},
3122
}
32-
},
33-
})
23+
}
24+
return {
25+
before: async (req) => {
26+
if (req.event.requestContext.http.method === 'OPTIONS') {
27+
console.debug(`[corsOPTIONS]`, `Handling OPTIONS request`)
28+
return {
29+
statusCode: 200,
30+
headers: corsHeaders(req.event, allowedMethods),
31+
}
32+
}
33+
return undefined
34+
},
35+
after: setCorsHeaders,
36+
onError: setCorsHeaders,
37+
}
38+
}

src/problemResponse.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { formatTypeBoxErrors } from '@hello.nrfcloud.com/proto'
2+
import {
3+
HttpStatusCode,
4+
type ProblemDetail,
5+
} from '@hello.nrfcloud.com/proto/hello'
6+
import type { MiddlewareObj } from '@middy/core'
7+
import type { Static } from '@sinclair/typebox'
8+
import type {
9+
APIGatewayProxyEventV2,
10+
APIGatewayProxyStructuredResultV2,
11+
Context,
12+
} from 'aws-lambda'
13+
import { aProblem } from './aProblem.js'
14+
import { ValidationFailedError } from './validateInput.js'
15+
16+
export class ProblemDetailError extends Error {
17+
public readonly problem: Static<typeof ProblemDetail>
18+
constructor(problem: Static<typeof ProblemDetail>) {
19+
super(problem.title)
20+
this.problem = problem
21+
this.name = 'ProblemDetailError'
22+
}
23+
}
24+
25+
export const problemResponse = (): MiddlewareObj<
26+
APIGatewayProxyEventV2,
27+
APIGatewayProxyStructuredResultV2,
28+
Error,
29+
Context
30+
> => ({
31+
onError: async (req) => {
32+
if (req.error instanceof ValidationFailedError) {
33+
return aProblem({
34+
title: 'Validation failed',
35+
status: HttpStatusCode.BAD_REQUEST,
36+
detail: formatTypeBoxErrors(req.error.errors),
37+
})
38+
}
39+
if (req.error instanceof ProblemDetailError) {
40+
return aProblem(req.error.problem)
41+
}
42+
return aProblem({
43+
title:
44+
req.error instanceof Error
45+
? req.error.message
46+
: 'Internal Server Error',
47+
status: HttpStatusCode.INTERNAL_SERVER_ERROR,
48+
})
49+
},
50+
})

src/validateInput.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
1+
import { validateWithTypeBox } from '@hello.nrfcloud.com/proto'
2+
import type { MiddlewareObj } from '@middy/core'
3+
import type { Static, TSchema } from '@sinclair/typebox'
4+
import type { ValueError } from '@sinclair/typebox/compiler'
15
import type {
26
APIGatewayProxyEventV2,
37
APIGatewayProxyStructuredResultV2,
48
Context,
59
} from 'aws-lambda'
6-
import type { MiddlewareObj } from '@middy/core'
7-
import type { Static, TSchema } from '@sinclair/typebox'
8-
import {
9-
formatTypeBoxErrors,
10-
validateWithTypeBox,
11-
} from '@hello.nrfcloud.com/proto'
12-
import { HttpStatusCode } from '@hello.nrfcloud.com/proto/hello'
1310
import { tryAsJSON } from './tryAsJSON.js'
14-
import { aProblem } from './aProblem.js'
11+
12+
export class ValidationFailedError extends Error {
13+
public readonly errors: ValueError[]
14+
constructor(errors: ValueError[]) {
15+
super('Validation failed')
16+
this.errors = errors
17+
this.name = 'ValidationFailedError'
18+
}
19+
}
1520

1621
export const validateInput = <Schema extends TSchema>(
1722
schema: Schema,
@@ -47,11 +52,7 @@ export const validateInput = <Schema extends TSchema>(
4752
`Input not valid`,
4853
JSON.stringify(maybeValidInput.errors),
4954
)
50-
return aProblem({
51-
title: 'Validation failed',
52-
status: HttpStatusCode.BAD_REQUEST,
53-
detail: formatTypeBoxErrors(maybeValidInput.errors),
54-
})
55+
throw new ValidationFailedError(maybeValidInput.errors)
5556
}
5657
console.debug(`[validateInput]`, `Input valid`)
5758
req.context.validInput = maybeValidInput.value

0 commit comments

Comments
 (0)