Skip to content

Commit 43dee8f

Browse files
Merge pull request #1054 from BitGo/beta
Release beta to production
2 parents 8b3e259 + 05db93f commit 43dee8f

File tree

11 files changed

+1535
-366
lines changed

11 files changed

+1535
-366
lines changed

package-lock.json

Lines changed: 371 additions & 280 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/express-wrapper/src/index.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,22 @@
66
import express from 'express';
77

88
import { ApiSpec, HttpRoute, Method as HttpMethod } from '@api-ts/io-ts-http';
9-
import { createRouter } from '@api-ts/typed-express-router';
9+
import {
10+
createRouter,
11+
DecodeErrorFormatterFn,
12+
EncodeErrorFormatterFn,
13+
GetDecodeErrorStatusCodeFn,
14+
GetEncodeErrorStatusCodeFn,
15+
} from '@api-ts/typed-express-router';
1016

11-
import { handleRequest, onDecodeError, onEncodeError, RouteHandler } from './request';
17+
import {
18+
handleRequest,
19+
defaultDecodeErrorFormatter,
20+
defaultEncodeErrorFormatter,
21+
defaultGetDecodeErrorStatusCode,
22+
defaultGetEncodeErrorStatusCode,
23+
RouteHandler,
24+
} from './request';
1225
import { defaultResponseEncoder, ResponseEncoder } from './response';
1326

1427
export { middlewareFn, MiddlewareChain, MiddlewareChainOutput } from './middleware';
@@ -25,16 +38,26 @@ type CreateRouterProps<Spec extends ApiSpec> = {
2538
};
2639
};
2740
encoder?: ResponseEncoder;
41+
decodeErrorFormatter?: DecodeErrorFormatterFn;
42+
encodeErrorFormatter?: EncodeErrorFormatterFn;
43+
getDecodeErrorStatusCode?: GetDecodeErrorStatusCodeFn;
44+
getEncodeErrorStatusCode?: GetEncodeErrorStatusCodeFn;
2845
};
2946

3047
export function routerForApiSpec<Spec extends ApiSpec>({
3148
spec,
3249
routeHandlers,
3350
encoder = defaultResponseEncoder,
51+
decodeErrorFormatter = defaultDecodeErrorFormatter,
52+
encodeErrorFormatter = defaultEncodeErrorFormatter,
53+
getDecodeErrorStatusCode = defaultGetDecodeErrorStatusCode,
54+
getEncodeErrorStatusCode = defaultGetEncodeErrorStatusCode,
3455
}: CreateRouterProps<Spec>) {
3556
const router = createRouter(spec, {
36-
onDecodeError,
37-
onEncodeError,
57+
decodeErrorFormatter,
58+
encodeErrorFormatter,
59+
getDecodeErrorStatusCode,
60+
getEncodeErrorStatusCode,
3861
});
3962
for (const apiName of Object.keys(spec)) {
4063
const resource = spec[apiName] as Spec[string];

packages/express-wrapper/src/request.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import * as PathReporter from 'io-ts/lib/PathReporter';
88

99
import { ApiSpec, HttpRoute, RequestType, ResponseType } from '@api-ts/io-ts-http';
1010
import {
11-
OnDecodeErrorFn,
12-
OnEncodeErrorFn,
11+
type DecodeErrorFormatterFn,
12+
type EncodeErrorFormatterFn,
13+
type GetDecodeErrorStatusCodeFn,
14+
type GetEncodeErrorStatusCodeFn,
1315
TypedRequestHandler,
1416
} from '@api-ts/typed-express-router';
1517

@@ -94,17 +96,28 @@ const createNamedFunction = <F extends (...args: any) => void>(
9496
fn: F,
9597
): F => Object.defineProperty(fn, 'name', { value: name });
9698

97-
export const onDecodeError: OnDecodeErrorFn = (errs, _req, res) => {
99+
export const defaultDecodeErrorFormatter: DecodeErrorFormatterFn = (errs, _req) => {
98100
const validationErrors = PathReporter.failure(errs);
99-
const validationErrorMessage = validationErrors.join('\n');
100-
res.writeHead(400, { 'Content-Type': 'application/json' });
101-
res.write(JSON.stringify({ error: validationErrorMessage }));
102-
res.end();
101+
return { error: validationErrors.join('\n') };
103102
};
104103

105-
export const onEncodeError: OnEncodeErrorFn = (err, _req, res) => {
104+
export const defaultEncodeErrorFormatter: EncodeErrorFormatterFn = (_err, _req) => {
105+
return {};
106+
};
107+
108+
export const defaultGetDecodeErrorStatusCode: GetDecodeErrorStatusCodeFn = (
109+
_errs,
110+
_req,
111+
) => {
112+
return 400;
113+
};
114+
115+
export const defaultGetEncodeErrorStatusCode: GetEncodeErrorStatusCodeFn = (
116+
err,
117+
_req,
118+
) => {
106119
console.warn('Error in route handler:', err);
107-
res.status(500).end();
120+
return 500;
108121
};
109122

110123
export const handleRequest = (

packages/typed-express-router/README.md

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,19 @@ logging.
7676

7777
```ts
7878
const typedRouter = createRouter(MyApi, {
79-
onDecodeError: (errs, req, res) => {
79+
decodeErrorFormatter: (errs, req) => {
8080
// Format `errs` however you want
81-
res.send(400).json({ message: 'Bad request' }).end();
81+
return { message: 'Bad request' };
8282
},
83-
onEncodeError: (err, req, res) => {
83+
getDecodeErrorStatusCode: (errs, req) => {
84+
return 400;
85+
},
86+
encodeErrorFormatter: (err, req) => {
87+
return { message: 'Internal server error' };
88+
},
89+
getEncodeErrorStatusCode: (err, req) => {
8490
// Ideally won't happen unless type safety is violated, so it's a 500
85-
res.send(500).json({ message: 'Internal server error' }).end();
91+
return 500;
8692
},
8793
afterEncodedResponseSent: (status, payload, req, res) => {
8894
// Perform side effects or other things, `res` should be ended by this point
@@ -92,17 +98,18 @@ const typedRouter = createRouter(MyApi, {
9298

9399
// Override the decode error handler on one route
94100
typedRouter.get('hello.world', [HelloWorldHandler], {
95-
onDecodeError: customHelloDecodeErrorHandler,
101+
decodeErrorFormatter: customHelloDecodeErrorFormatter,
96102
});
97103
```
98104

99105
### Unchecked routes
100106

101107
If you need custom behavior on decode errors that is more involved than just sending an
102108
error response, then the unchecked variant of the router functions can be used. They do
103-
not fail and call `onDecodeError` when a request is invalid. Instead, they will still
104-
populate `req.decoded`, except this time it'll contain the
105-
`Either<Errors, DecodedRequest>` type for route handlers to inspect.
109+
not fail and send a http response using `decodeErrorFormatter` and
110+
`getDecodeErrorStatusCode` when a request is invalid. Instead, they will still populate
111+
`req.decoded`, except this time it'll contain the `Either<Errors, DecodedRequest>` type
112+
for route handlers to inspect.
106113

107114
```ts
108115
// Just a normal express route

packages/typed-express-router/package.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,22 @@
2323
"fp-ts": "^2.0.0",
2424
"io-ts": "2.1.3"
2525
},
26+
"peerDependencies": {
27+
"@opentelemetry/api": "^1.0.0"
28+
},
29+
"peerDependenciesMeta": {
30+
"@opentelemetry/api": {
31+
"optional": true
32+
}
33+
},
2634
"devDependencies": {
2735
"@api-ts/superagent-wrapper": "0.0.0-semantically-released",
2836
"@swc-node/register": "1.10.9",
2937
"c8": "10.1.3",
30-
"typescript": "4.7.4"
38+
"typescript": "4.7.4",
39+
"@opentelemetry/sdk-trace-base": "1.30.1",
40+
"@opentelemetry/sdk-trace-node": "1.30.1",
41+
"@opentelemetry/api": "1.9.0"
3142
},
3243
"publishConfig": {
3344
"access": "public"
Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
1-
import express from 'express';
2-
import { Errors } from 'io-ts';
31
import * as PathReporter from 'io-ts/lib/PathReporter';
42

5-
export function defaultOnDecodeError(
6-
errs: Errors,
7-
_req: express.Request,
8-
res: express.Response,
9-
) {
10-
const validationErrors = PathReporter.failure(errs);
11-
const validationErrorMessage = validationErrors.join('\n');
12-
res.status(400).json({ error: validationErrorMessage }).end();
13-
}
3+
import type {
4+
DecodeErrorFormatterFn,
5+
EncodeErrorFormatterFn,
6+
GetDecodeErrorStatusCodeFn,
7+
GetEncodeErrorStatusCodeFn,
8+
} from './types';
149

15-
export function defaultOnEncodeError(
16-
err: unknown,
17-
_req: express.Request,
18-
res: express.Response,
19-
) {
20-
res.status(500).end();
21-
console.warn(`Error in route handler: ${err}`);
22-
}
10+
export const defaultDecodeErrorFormatter: DecodeErrorFormatterFn = PathReporter.failure;
11+
12+
export const defaultGetDecodeErrorStatusCode: GetDecodeErrorStatusCodeFn = (
13+
_err,
14+
_req,
15+
) => 400;
16+
17+
export const defaultEncodeErrorFormatter: EncodeErrorFormatterFn = () => ({});
18+
19+
export const defaultGetEncodeErrorStatusCode: GetEncodeErrorStatusCodeFn = (
20+
_err,
21+
_req,
22+
) => 500;

packages/typed-express-router/src/index.ts

Lines changed: 78 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,27 @@
33
*/
44

55
import { ApiSpec, HttpRoute, KeyToHttpStatus } from '@api-ts/io-ts-http';
6+
import type { Span } from '@opentelemetry/api';
67
import express from 'express';
78
import * as E from 'fp-ts/Either';
89
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';
1017
import { apiTsPathToExpress } from './path';
18+
import {
19+
ApiTsAttributes,
20+
createDecodeSpan,
21+
createSendEncodedSpan,
22+
endSpan,
23+
recordSpanDecodeError,
24+
recordSpanEncodeError,
25+
setSpanAttributes,
26+
} from './telemetry';
1127
import {
1228
AddRouteHandler,
1329
AddUncheckedRouteHandler,
@@ -22,8 +38,10 @@ import {
2238

2339
export type {
2440
AfterEncodedResponseSentFn,
25-
OnDecodeErrorFn,
26-
OnEncodeErrorFn,
41+
DecodeErrorFormatterFn,
42+
EncodeErrorFormatterFn,
43+
GetDecodeErrorStatusCodeFn,
44+
GetEncodeErrorStatusCodeFn,
2745
TypedRequestHandler,
2846
UncheckedRequestHandler,
2947
WrappedRouter,
@@ -43,17 +61,21 @@ export type {
4361
export function createRouter<Spec extends ApiSpec>(
4462
spec: Spec,
4563
{
46-
onDecodeError,
47-
onEncodeError,
64+
encodeErrorFormatter,
65+
getEncodeErrorStatusCode,
4866
afterEncodedResponseSent,
67+
decodeErrorFormatter,
68+
getDecodeErrorStatusCode,
4969
...options
5070
}: WrappedRouterOptions = {},
5171
): WrappedRouter<Spec> {
5272
const router = express.Router(options);
5373
return wrapRouter(router, spec, {
54-
onDecodeError,
55-
onEncodeError,
74+
encodeErrorFormatter,
75+
getEncodeErrorStatusCode,
5676
afterEncodedResponseSent,
77+
decodeErrorFormatter,
78+
getDecodeErrorStatusCode,
5779
});
5880
}
5981

@@ -69,9 +91,11 @@ export function wrapRouter<Spec extends ApiSpec>(
6991
router: express.Router,
7092
spec: Spec,
7193
{
72-
onDecodeError = defaultOnDecodeError,
73-
onEncodeError = defaultOnEncodeError,
94+
encodeErrorFormatter = defaultEncodeErrorFormatter,
95+
getEncodeErrorStatusCode = defaultGetEncodeErrorStatusCode,
7496
afterEncodedResponseSent = () => {},
97+
decodeErrorFormatter = defaultDecodeErrorFormatter,
98+
getDecodeErrorStatusCode = defaultGetDecodeErrorStatusCode,
7599
}: WrappedRouteOptions,
76100
): WrappedRouter<Spec> {
77101
const routerMiddleware: UncheckedRequestHandler[] = [];
@@ -81,12 +105,14 @@ export function wrapRouter<Spec extends ApiSpec>(
81105
): AddUncheckedRouteHandler<Spec, Method> {
82106
return (apiName, handlers, options) => {
83107
const route: HttpRoute | undefined = spec[apiName]?.[method];
108+
let decodeSpan: Span | undefined;
84109
if (route === undefined) {
85110
// Should only happen with an explicit undefined property, which we can only prevent at the
86111
// type level with the `exactOptionalPropertyTypes` tsconfig option
87112
throw Error(`Method "${method}" at "${apiName}" must not be "undefined"'`);
88113
}
89114
const wrapReqAndRes: UncheckedRequestHandler = (req, res, next) => {
115+
decodeSpan = createDecodeSpan({ apiName, httpRoute: route });
90116
// Intentionally passing explicit arguments here instead of decoding
91117
// req by itself because of issues that arise while using Node 16
92118
// See https://github.com/BitGo/api-ts/pull/394 for more information.
@@ -103,6 +129,10 @@ export function wrapRouter<Spec extends ApiSpec>(
103129
status: keyof (typeof route)['response'],
104130
payload: unknown,
105131
) => {
132+
const encodeSpan = createSendEncodedSpan({
133+
apiName,
134+
httpRoute: route,
135+
});
106136
try {
107137
const codec = route.response[status];
108138
if (!codec) {
@@ -112,6 +142,9 @@ export function wrapRouter<Spec extends ApiSpec>(
112142
typeof status === 'number'
113143
? status
114144
: KeyToHttpStatus[status as keyof KeyToHttpStatus];
145+
setSpanAttributes(encodeSpan, {
146+
[ApiTsAttributes.API_TS_STATUS_CODE]: statusCode,
147+
});
115148
if (statusCode === undefined) {
116149
throw new Error(`unknown HTTP status code for key ${status}`);
117150
} else if (!codec.is(payload)) {
@@ -126,18 +159,42 @@ export function wrapRouter<Spec extends ApiSpec>(
126159
res as WrappedResponse,
127160
);
128161
} 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);
134173
}
135174
};
136175
next();
137176
};
138177

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+
139195
const middlewareChain = [
140196
wrapReqAndRes,
197+
endDecodeSpanMiddleware,
141198
...routerMiddleware,
142199
...handlers,
143200
] as express.RequestHandler[];
@@ -164,7 +221,13 @@ export function wrapRouter<Spec extends ApiSpec>(
164221
req.decoded,
165222
E.matchW(
166223
(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);
168231
},
169232
(value) => {
170233
req.decoded = value;

0 commit comments

Comments
 (0)