Skip to content

Commit c1af7da

Browse files
wip: checkpoint
Back to you, @starfy84! Co-authored-by: Dereck Tu <[email protected]>
1 parent a0275b1 commit c1af7da

File tree

6 files changed

+131
-155
lines changed

6 files changed

+131
-155
lines changed

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@ import express from 'express';
22
import { Errors } from 'io-ts';
33
import * as PathReporter from 'io-ts/lib/PathReporter';
44

5+
import { OnDecodeErrorFn } from './types';
6+
57
export function defaultOnDecodeError(
68
errs: Errors,
79
_req: express.Request,
8-
res: express.Response,
9-
) {
10+
_res: express.Response,
11+
): ReturnType<OnDecodeErrorFn> {
1012
const validationErrors = PathReporter.failure(errs);
1113
const validationErrorMessage = validationErrors.join('\n');
12-
res.status(400).json({ error: validationErrorMessage }).end();
14+
return { response: validationErrorMessage, statusCode: 400 };
1315
}
1416

1517
export function defaultOnEncodeError(

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

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export function wrapRouter<Spec extends ApiSpec>(
116116
status: keyof (typeof route)['response'],
117117
payload: unknown,
118118
) => {
119-
const span = createSendEncodedSpan({
119+
const encodeSpan = createSendEncodedSpan({
120120
apiName,
121121
httpRoute: route,
122122
});
@@ -129,7 +129,7 @@ export function wrapRouter<Spec extends ApiSpec>(
129129
typeof status === 'number'
130130
? status
131131
: KeyToHttpStatus[status as keyof KeyToHttpStatus];
132-
setSpanAttributes(span, {
132+
setSpanAttributes(encodeSpan, {
133133
[ApiTsAttributes.API_TS_STATUS_CODE]: statusCode,
134134
});
135135
if (statusCode === undefined) {
@@ -146,28 +146,26 @@ export function wrapRouter<Spec extends ApiSpec>(
146146
res as WrappedResponse,
147147
);
148148
} catch (err) {
149-
recordSpanEncodeError(span, err);
149+
recordSpanEncodeError(encodeSpan, err);
150150
(options?.onEncodeError ?? onEncodeError)(
151151
err,
152152
req as WrappedRequest,
153153
res as WrappedResponse,
154154
);
155155
} finally {
156-
endSpan(span);
156+
endSpan(encodeSpan);
157157
}
158158
};
159159
next();
160160
};
161161

162-
const endDecodeSpanMiddleware: UncheckedRequestHandler = (req, _, next) => {
162+
const endDecodeSpanMiddleware: UncheckedRequestHandler = (req, res, next) => {
163163
pipe(
164164
req.decoded,
165-
E.matchW(
166-
(errs) => {
167-
recordSpanDecodeError(decodeSpan, errs);
168-
},
169-
() => {}, // No need to do anything on success
170-
),
165+
E.getOrElseW((errs) => {
166+
const error = (onDecodeError ?? onDecodeError)(errs, req, res);
167+
recordSpanDecodeError(decodeSpan, error.response, error.statusCode);
168+
}),
171169
);
172170
endSpan(decodeSpan);
173171
next();
@@ -202,7 +200,10 @@ export function wrapRouter<Spec extends ApiSpec>(
202200
req.decoded,
203201
E.matchW(
204202
(errs) => {
205-
(options?.onDecodeError ?? onDecodeError)(errs, req, res);
203+
const { response, statusCode } = (
204+
options?.onDecodeError ?? onDecodeError
205+
)(errs, req, res);
206+
res.status(statusCode).json(response).end();
206207
},
207208
(value) => {
208209
req.decoded = value;

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

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,43 +5,42 @@
55
// This module handles the optional dependency on @opentelemetry/api
66
// It provides functionality for creating spans for decode and sendEncoded operations
77

8-
import { Errors } from 'io-ts';
9-
import type { SpanMetadata } from './types';
108
import type { Attributes, Span, Tracer } from '@opentelemetry/api';
11-
import * as PathReporter from 'io-ts/lib/PathReporter';
9+
10+
import type { Json, SpanMetadata } from './types';
1211

1312
let otelApi: any;
1413
let tracer: Tracer | undefined;
1514

16-
// Try to load @opentelemetry/api if available
15+
// Load @opentelemetry/api, if available.
1716
try {
1817
otelApi = require('@opentelemetry/api');
1918
if (otelApi) {
2019
tracer = otelApi.trace.getTracer('typed-express-router');
2120
}
2221
} catch (e) {
23-
// Optional dependency not available, tracing will be disabled
22+
// Optional dependency not available, so tracing will be disabled.
2423
tracer = undefined;
2524
}
2625

2726
export const ApiTsAttributes = {
2827
/**
29-
* Route name in the API-TS spec
28+
* The Operation ID of the HTTP request
3029
*/
31-
API_TS_ROUTE_NAME: 'api_ts.route.name',
30+
API_TS_OPERATION_ID: 'api_ts.operation_id',
3231

3332
/**
34-
* HTTP method for the route
33+
* The method of the HTTP request
3534
*/
36-
API_TS_ROUTE_METHOD: 'api_ts.route.method',
35+
API_TS_METHOD: 'api_ts.method',
3736

3837
/**
39-
* Path for the route
38+
* The path of the HTTP request
4039
*/
41-
API_TS_ROUTE_PATH: 'api_ts.route.path',
40+
API_TS_PATH: 'api_ts.path',
4241

4342
/**
44-
* Request status code
43+
* Returned HTTP request status code
4544
*/
4645
API_TS_STATUS_CODE: 'api_ts.status_code',
4746
};
@@ -56,15 +55,15 @@ export function createDefaultDecodeAttributes(metadata: SpanMetadata): Attribute
5655
const attributes: Attributes = {};
5756

5857
if (metadata.apiName) {
59-
attributes[ApiTsAttributes.API_TS_ROUTE_NAME] = metadata.apiName;
58+
attributes[ApiTsAttributes.API_TS_OPERATION_ID] = metadata.apiName;
6059
}
6160

6261
if (metadata.httpRoute) {
6362
if (metadata.httpRoute.method) {
64-
attributes[ApiTsAttributes.API_TS_ROUTE_METHOD] = metadata.httpRoute.method;
63+
attributes[ApiTsAttributes.API_TS_METHOD] = metadata.httpRoute.method;
6564
}
6665
if (metadata.httpRoute.path) {
67-
attributes[ApiTsAttributes.API_TS_ROUTE_PATH] = metadata.httpRoute.path;
66+
attributes[ApiTsAttributes.API_TS_PATH] = metadata.httpRoute.path;
6867
}
6968
}
7069

@@ -81,15 +80,15 @@ export function createDefaultEncodeAttributes(metadata: SpanMetadata): Attribute
8180
const attributes: Attributes = {};
8281

8382
if (metadata.apiName) {
84-
attributes[ApiTsAttributes.API_TS_ROUTE_NAME] = metadata.apiName;
83+
attributes[ApiTsAttributes.API_TS_OPERATION_ID] = metadata.apiName;
8584
}
8685

8786
if (metadata.httpRoute) {
8887
if (metadata.httpRoute.method) {
89-
attributes[ApiTsAttributes.API_TS_ROUTE_METHOD] = metadata.httpRoute.method;
88+
attributes[ApiTsAttributes.API_TS_METHOD] = metadata.httpRoute.method;
9089
}
9190
if (metadata.httpRoute.path) {
92-
attributes[ApiTsAttributes.API_TS_ROUTE_PATH] = metadata.httpRoute.path;
91+
attributes[ApiTsAttributes.API_TS_PATH] = metadata.httpRoute.path;
9392
}
9493
}
9594

@@ -148,22 +147,28 @@ export function recordSpanEncodeError(span: Span | undefined, error: unknown): v
148147
/**
149148
* Records errors on a decode span
150149
* @param span The span to record the errors on
151-
* @param errors The errors to record
150+
* @param error The JSON error value to record
151+
* @param statusCode The HTTP status code
152152
*/
153-
export function recordSpanDecodeError(span: Span | undefined, errors: Errors): void {
153+
export function recordSpanDecodeError(
154+
span: Span | undefined,
155+
error: Json,
156+
statusCode: number,
157+
): void {
154158
if (!span || !otelApi) {
155159
return;
156160
}
157-
const validationErrors = PathReporter.failure(errors);
158-
const validationErrorMessage = validationErrors.join('\n');
159-
span.recordException(new Error(validationErrorMessage));
161+
setSpanAttributes(span, {
162+
[ApiTsAttributes.API_TS_STATUS_CODE]: statusCode,
163+
});
164+
span.recordException(JSON.stringify(error, null, 2));
160165
span.setStatus({ code: otelApi.SpanStatusCode.ERROR });
161166
}
162167

163168
/**
164169
* Sets a span's attributes if it exists
165170
* @param span The span to modify
166-
* @param errors The attributes to modify the span with
171+
* @param attributes The attributes to modify the span with
167172
*/
168173
export function setSpanAttributes(
169174
span: Span | undefined,

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ import { ApiSpec, HttpRoute, Method as HttpMethod } from '@api-ts/io-ts-http';
22
import express from 'express';
33
import * as t from 'io-ts';
44

5+
export type Json = boolean | number | string | null | JsonArray | JsonRecord;
6+
export interface JsonRecord {
7+
readonly [key: string]: Json;
8+
}
9+
export interface JsonArray extends ReadonlyArray<Json> {}
10+
511
export type Methods = Lowercase<HttpMethod>;
612

713
export type WrappedRequest<Decoded = unknown> = express.Request & {
@@ -22,7 +28,7 @@ export type OnDecodeErrorFn = (
2228
errs: t.Errors,
2329
req: express.Request,
2430
res: express.Response,
25-
) => void;
31+
) => { response: Json; statusCode: number };
2632

2733
export type OnEncodeErrorFn = (
2834
err: unknown,

packages/typed-express-router/test/server.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -361,8 +361,8 @@ test('should return a 400 when request fails to decode', async () => {
361361

362362
test('should invoke custom decode error function', async () => {
363363
const router = createRouter(TestApiSpec, {
364-
onDecodeError: (_errs, _req, res) => {
365-
res.status(400).json('Custom decode error').end();
364+
onDecodeError: (_errs, _req, _res) => {
365+
return { response: 'Custom decode error', statusCode: 400 };
366366
},
367367
});
368368

@@ -390,8 +390,8 @@ test('should invoke custom decode error function', async () => {
390390

391391
test('should invoke per-route custom decode error function', async () => {
392392
const router = createRouter(TestApiSpec, {
393-
onDecodeError: (_errs, _req, res) => {
394-
res.status(400).json('Top-level decode error').end();
393+
onDecodeError: (_errs, _req, _res) => {
394+
return { response: 'Top-level decode error', statusCode: 400 };
395395
},
396396
});
397397

@@ -403,8 +403,8 @@ test('should invoke per-route custom decode error function', async () => {
403403
},
404404
],
405405
{
406-
onDecodeError: (_errs, _req, res) => {
407-
res.status(400).json('Route decode error').end();
406+
onDecodeError: (_errs, _req, _res) => {
407+
return { response: 'Route decode error', statusCode: 400 };
408408
},
409409
routeAliases: ['/helloNoPathParams'],
410410
},

0 commit comments

Comments
 (0)