3
3
*/
4
4
5
5
import { ApiSpec , HttpRoute , KeyToHttpStatus } from '@api-ts/io-ts-http' ;
6
+ import type { Span } from '@opentelemetry/api' ;
6
7
import express from 'express' ;
7
8
import * as E from 'fp-ts/Either' ;
8
9
import { pipe } from 'fp-ts/pipeable' ;
9
- import { defaultOnDecodeError , defaultOnEncodeError } from './errors' ;
10
+
11
+ import {
12
+ defaultDecodeErrorFormatter ,
13
+ defaultEncodeErrorFormatter ,
14
+ defaultGetDecodeErrorStatusCode ,
15
+ defaultGetEncodeErrorStatusCode ,
16
+ } from './errors' ;
10
17
import { apiTsPathToExpress } from './path' ;
18
+ import {
19
+ ApiTsAttributes ,
20
+ createDecodeSpan ,
21
+ createSendEncodedSpan ,
22
+ endSpan ,
23
+ recordSpanDecodeError ,
24
+ recordSpanEncodeError ,
25
+ setSpanAttributes ,
26
+ } from './telemetry' ;
11
27
import {
12
28
AddRouteHandler ,
13
29
AddUncheckedRouteHandler ,
@@ -22,8 +38,10 @@ import {
22
38
23
39
export type {
24
40
AfterEncodedResponseSentFn ,
25
- OnDecodeErrorFn ,
26
- OnEncodeErrorFn ,
41
+ DecodeErrorFormatterFn ,
42
+ EncodeErrorFormatterFn ,
43
+ GetDecodeErrorStatusCodeFn ,
44
+ GetEncodeErrorStatusCodeFn ,
27
45
TypedRequestHandler ,
28
46
UncheckedRequestHandler ,
29
47
WrappedRouter ,
@@ -43,17 +61,21 @@ export type {
43
61
export function createRouter < Spec extends ApiSpec > (
44
62
spec : Spec ,
45
63
{
46
- onDecodeError ,
47
- onEncodeError ,
64
+ encodeErrorFormatter ,
65
+ getEncodeErrorStatusCode ,
48
66
afterEncodedResponseSent,
67
+ decodeErrorFormatter,
68
+ getDecodeErrorStatusCode,
49
69
...options
50
70
} : WrappedRouterOptions = { } ,
51
71
) : WrappedRouter < Spec > {
52
72
const router = express . Router ( options ) ;
53
73
return wrapRouter ( router , spec , {
54
- onDecodeError ,
55
- onEncodeError ,
74
+ encodeErrorFormatter ,
75
+ getEncodeErrorStatusCode ,
56
76
afterEncodedResponseSent,
77
+ decodeErrorFormatter,
78
+ getDecodeErrorStatusCode,
57
79
} ) ;
58
80
}
59
81
@@ -69,9 +91,11 @@ export function wrapRouter<Spec extends ApiSpec>(
69
91
router : express . Router ,
70
92
spec : Spec ,
71
93
{
72
- onDecodeError = defaultOnDecodeError ,
73
- onEncodeError = defaultOnEncodeError ,
94
+ encodeErrorFormatter = defaultEncodeErrorFormatter ,
95
+ getEncodeErrorStatusCode = defaultGetEncodeErrorStatusCode ,
74
96
afterEncodedResponseSent = ( ) => { } ,
97
+ decodeErrorFormatter = defaultDecodeErrorFormatter ,
98
+ getDecodeErrorStatusCode = defaultGetDecodeErrorStatusCode ,
75
99
} : WrappedRouteOptions ,
76
100
) : WrappedRouter < Spec > {
77
101
const routerMiddleware : UncheckedRequestHandler [ ] = [ ] ;
@@ -81,12 +105,14 @@ export function wrapRouter<Spec extends ApiSpec>(
81
105
) : AddUncheckedRouteHandler < Spec , Method > {
82
106
return ( apiName , handlers , options ) => {
83
107
const route : HttpRoute | undefined = spec [ apiName ] ?. [ method ] ;
108
+ let decodeSpan : Span | undefined ;
84
109
if ( route === undefined ) {
85
110
// Should only happen with an explicit undefined property, which we can only prevent at the
86
111
// type level with the `exactOptionalPropertyTypes` tsconfig option
87
112
throw Error ( `Method "${ method } " at "${ apiName } " must not be "undefined"'` ) ;
88
113
}
89
114
const wrapReqAndRes : UncheckedRequestHandler = ( req , res , next ) => {
115
+ decodeSpan = createDecodeSpan ( { apiName, httpRoute : route } ) ;
90
116
// Intentionally passing explicit arguments here instead of decoding
91
117
// req by itself because of issues that arise while using Node 16
92
118
// See https://github.com/BitGo/api-ts/pull/394 for more information.
@@ -103,6 +129,10 @@ export function wrapRouter<Spec extends ApiSpec>(
103
129
status : keyof ( typeof route ) [ 'response' ] ,
104
130
payload : unknown ,
105
131
) => {
132
+ const encodeSpan = createSendEncodedSpan ( {
133
+ apiName,
134
+ httpRoute : route ,
135
+ } ) ;
106
136
try {
107
137
const codec = route . response [ status ] ;
108
138
if ( ! codec ) {
@@ -112,6 +142,9 @@ export function wrapRouter<Spec extends ApiSpec>(
112
142
typeof status === 'number'
113
143
? status
114
144
: KeyToHttpStatus [ status as keyof KeyToHttpStatus ] ;
145
+ setSpanAttributes ( encodeSpan , {
146
+ [ ApiTsAttributes . API_TS_STATUS_CODE ] : statusCode ,
147
+ } ) ;
115
148
if ( statusCode === undefined ) {
116
149
throw new Error ( `unknown HTTP status code for key ${ status } ` ) ;
117
150
} else if ( ! codec . is ( payload ) ) {
@@ -126,18 +159,42 @@ export function wrapRouter<Spec extends ApiSpec>(
126
159
res as WrappedResponse ,
127
160
) ;
128
161
} catch ( err ) {
129
- ( options ?. onEncodeError ?? onEncodeError ) (
130
- err ,
131
- req as WrappedRequest ,
132
- res as WrappedResponse ,
133
- ) ;
162
+ const statusCode = (
163
+ options ?. getEncodeErrorStatusCode ?? getEncodeErrorStatusCode
164
+ ) ( err , req ) ;
165
+ const encodeErrorMessage = (
166
+ options ?. encodeErrorFormatter ?? encodeErrorFormatter
167
+ ) ( err , req ) ;
168
+
169
+ recordSpanEncodeError ( encodeSpan , err , statusCode ) ;
170
+ res . status ( statusCode ) . json ( encodeErrorMessage ) ;
171
+ } finally {
172
+ endSpan ( encodeSpan ) ;
134
173
}
135
174
} ;
136
175
next ( ) ;
137
176
} ;
138
177
178
+ const endDecodeSpanMiddleware : UncheckedRequestHandler = ( req , _res , next ) => {
179
+ pipe (
180
+ req . decoded ,
181
+ E . getOrElseW ( ( errs ) => {
182
+ const decodeErrorMessage = (
183
+ options ?. decodeErrorFormatter ?? decodeErrorFormatter
184
+ ) ( errs , req ) ;
185
+ const statusCode = (
186
+ options ?. getDecodeErrorStatusCode ?? getDecodeErrorStatusCode
187
+ ) ( errs , req ) ;
188
+ recordSpanDecodeError ( decodeSpan , decodeErrorMessage , statusCode ) ;
189
+ } ) ,
190
+ ) ;
191
+ endSpan ( decodeSpan ) ;
192
+ next ( ) ;
193
+ } ;
194
+
139
195
const middlewareChain = [
140
196
wrapReqAndRes ,
197
+ endDecodeSpanMiddleware ,
141
198
...routerMiddleware ,
142
199
...handlers ,
143
200
] as express . RequestHandler [ ] ;
@@ -164,7 +221,13 @@ export function wrapRouter<Spec extends ApiSpec>(
164
221
req . decoded ,
165
222
E . matchW (
166
223
( errs ) => {
167
- ( options ?. onDecodeError ?? onDecodeError ) ( errs , req , res ) ;
224
+ const statusCode = (
225
+ options ?. getDecodeErrorStatusCode ?? getDecodeErrorStatusCode
226
+ ) ( errs , req ) ;
227
+ const decodeErrorMessage = (
228
+ options ?. decodeErrorFormatter ?? decodeErrorFormatter
229
+ ) ( errs , req ) ;
230
+ res . status ( statusCode ) . json ( decodeErrorMessage ) ;
168
231
} ,
169
232
( value ) => {
170
233
req . decoded = value ;
0 commit comments