Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "crosswalk",
"version": "2.2.4",
"version": "2.2.5",
"description": "Type-safe express routing with TypeScript",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
63 changes: 62 additions & 1 deletion src/__tests__/typed-router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ test('Throwing HTTPError should set status code', async () => {
name: 'John',
age: 34,
phoneNumbers: [],
role: 'user',
role: 'user' as const,
permanentAddress: {
street: '123 Main St',
city: 'Anytown',
Expand Down Expand Up @@ -525,6 +525,67 @@ test('Throwing HTTPError should set status code', async () => {
expect(r.body).toEqual({});
});

test('Throwing HTTPError should set status code in non-async handlers', async () => {
const app = express();
const router = new TypedRouter<API>(app, apiSchemaJson);

class PGError extends Error {
constructor(public code: string) {
super();
}
}

router.get('/users', async () => {
return {users: [] as User[]};
});

router.get('/users/:userId', ({userId}) => {
if (userId === 'throw-400') {
throw new HTTPError(400, 'Very bad request');
} else if (userId === 'throw-pg-error') {
// See https://github.com/danvk/crosswalk/issues/6
throw new PGError('23505');
}

return Promise.resolve({
id: '1',
name: 'John',
age: 34,
phoneNumbers: [],
role: 'user' as const,
permanentAddress: {
street: '123 Main St',
city: 'Anytown',
state: 'CA',
zip: '12345',
location: {
latitude: 37.774929,
longitude: -122.419416,
},
},
});
});

const api = request(app);
let r = await api.get('/users/fred').expect(200);
expect(r.body).toMatchObject({
name: 'John',
age: 34,
});

r = await api.get('/users?minAge=Fred').expect(400);
expect(r.body).toMatchObject({error: 'data/minAge must be number'});

r = await api.get('/users?maxAge=5').expect(400);
expect(r.body).toMatchObject({error: 'data must NOT have additional properties'});

r = await api.get('/users/throw-400').expect(400);
expect(r.body).toMatchObject({error: 'Very bad request'});

r = await api.get('/users/throw-pg-error').expect(500);
expect(r.body).toEqual({});
});

test('Custom 400 handler', async () => {
const app = express();
const router = new TypedRouter<API>(app, apiSchemaJson, {
Expand Down
46 changes: 23 additions & 23 deletions src/typed-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export class TypedRouter<API> {
const queryValidate = this.getValidator(route, method, 'query');
this.registrations.push({path: route as string, method});

let handlerFn = (...[req, response, next]: RequestParams) => {
let handlerFn = async (...[req, response, next]: RequestParams) => {
const {body, query} = req;

if (bodyValidate && !bodyValidate(body)) {
Expand Down Expand Up @@ -183,33 +183,33 @@ export class TypedRouter<API> {
console.debug(method, route, 'params=', req.params, 'body=', body, 'query=', query);
}

handler(req.params as any, body, req as any, response)
.then(responseObject => {
if (responseObject === null) {
// nothing to do. This can happen if the response redirected, say.
} else if (typeof responseObject === 'string') {
response.status(200).send(responseObject);
} else {
response.json(responseObject);
}
})
.catch((error: any) => {
// With target below ES2015, instanceof doesn't work here.
if (
error instanceof HTTPError ||
(error.code && STATUS_CODES.indexOf(error.code) >= 0)
) {
response.status(error.code).json({error: error.message});
} else {
next(error);
}
});
try {
const responseObject = await handler(req.params as any, body, req as any, response);

if (responseObject === null) {
// nothing to do. This can happen if the response redirected, say.
} else if (typeof responseObject === 'string') {
response.status(200).send(responseObject);
} else {
response.json(responseObject);
}
} catch (error: any) {
// Handle synchronous throws
if (
error instanceof HTTPError ||
(error.code && STATUS_CODES.indexOf(error.code) >= 0)
) {
response.status(error.code).json({error: error.message});
} else {
next(error);
}
}
};

for (let i = this.middlewareFns.length - 1; i >= 0; i--) {
const middlewareFn = this.middlewareFns[i];
const prevHandlerFn = handlerFn;
handlerFn = (req: any, res: any, next: any) =>
handlerFn = async (req: any, res: any, next: any) =>
middlewareFn(req, res, () => prevHandlerFn(req, res, next));
}

Expand Down