From e9537c1b5961fc07898c8383dbefc7038b8cab64 Mon Sep 17 00:00:00 2001 From: Patrick McLaughlin Date: Fri, 8 Jul 2022 11:43:38 -0400 Subject: [PATCH] feat: standardize `decodeExpecting` error messages --- packages/superagent-wrapper/src/request.ts | 29 +++++++++--- .../superagent-wrapper/test/request.test.ts | 46 ++++++++++++++----- 2 files changed, 57 insertions(+), 18 deletions(-) diff --git a/packages/superagent-wrapper/src/request.ts b/packages/superagent-wrapper/src/request.ts index 1bf27bfa..7f29286a 100644 --- a/packages/superagent-wrapper/src/request.ts +++ b/packages/superagent-wrapper/src/request.ts @@ -1,6 +1,7 @@ import * as h from '@api-ts/io-ts-http'; import * as E from 'fp-ts/Either'; import * as t from 'io-ts'; +import * as PathReporter from 'io-ts/lib/PathReporter'; import { URL } from 'url'; import { pipe } from 'fp-ts/function'; @@ -13,15 +14,24 @@ type SuccessfulResponses = { }; }[keyof Route['response']]; -type DecodedResponse = +export type DecodedResponse = | SuccessfulResponses | { status: 'decodeError'; - error: unknown; + error: string; body: unknown; original: Response; }; +export class DecodeError extends Error { + readonly decodedResponse: DecodedResponse; + + constructor(message: string, decodedResponse: DecodedResponse) { + super(message); + this.decodedResponse = decodedResponse; + } +} + const decodedResponse = (res: DecodedResponse) => res; type ExpectedDecodedResponse< @@ -134,7 +144,7 @@ const patchRequest = < // DISCUSS: what's this non-standard HTTP status code? decodedResponse({ status: 'decodeError', - error, + error: PathReporter.failure(error).join('\n'), body: res.body, original: res, }), @@ -146,9 +156,16 @@ const patchRequest = < status: StatusCode, ) => patchedReq.decode().then((res) => { - if (res.status !== status) { - const error = res.error ?? `Unexpected status code ${String(res.status)}`; - throw new Error(JSON.stringify(error)); + if (res.original.status !== status) { + const error = `Unexpected response ${String( + res.original.status, + )}: ${JSON.stringify(res.original.body)}`; + throw new DecodeError(error, res as DecodedResponse); + } else if (res.status === 'decodeError') { + const error = `Could not decode response ${String( + res.original.status, + )}: ${JSON.stringify(res.original.body)}`; + throw new DecodeError(error, res as DecodedResponse); } else { return res as ExpectedDecodedResponse; } diff --git a/packages/superagent-wrapper/test/request.test.ts b/packages/superagent-wrapper/test/request.test.ts index 8c4fe213..3669f6e3 100644 --- a/packages/superagent-wrapper/test/request.test.ts +++ b/packages/superagent-wrapper/test/request.test.ts @@ -9,7 +9,11 @@ import superagent from 'superagent'; import supertest from 'supertest'; import { URL } from 'url'; -import { superagentRequestFactory, supertestRequestFactory } from '../src/request'; +import { + DecodeError, + superagentRequestFactory, + supertestRequestFactory, +} from '../src/request'; import { buildApiClient } from '../src/routes'; const PostTestRoute = h.httpRoute({ @@ -33,6 +37,9 @@ const PostTestRoute = h.httpRoute({ bar: t.number, baz: t.boolean, }), + 401: t.type({ + message: t.string, + }), }, }); @@ -75,11 +82,16 @@ testApp.post('/test/:id', (req, res) => { res.send({ invalid: 'response', }); - } else if (req.headers['x-send-unexpected-status-code']) { + } else if (req.headers['x-send-unknown-status-code']) { res.status(400); res.send({ error: 'bad request', }); + } else if (req.headers['x-send-unexpected-status-code']) { + res.status(401); + res.send({ + message: 'unauthorized', + }); } else { const response = PostTestRoute.response[200].encode({ ...params, @@ -129,10 +141,10 @@ describe('request', () => { }); }); - it('gracefully handles unexpected status codes', async () => { + it('gracefully handles unknown status codes', async () => { const response = await apiClient['api.v1.test'] .post({ id: 1337, foo: 'test', bar: 42 }) - .set('x-send-unexpected-status-code', 'true') + .set('x-send-unknown-status-code', 'true') .decode(); assert.equal(response.status, 'decodeError'); @@ -169,10 +181,21 @@ describe('request', () => { .post({ id: 1337, foo: 'test', bar: 42 }) .set('x-send-unexpected-status-code', 'true') .decodeExpecting(200) - .then(() => false) - .catch(() => true); + .then(() => '') + .catch((err) => (err instanceof DecodeError ? err.message : '')); + + assert.deepEqual(result, 'Unexpected response 401: {"message":"unauthorized"}'); + }); + + it('throws for unknown responses', async () => { + const result = await apiClient['api.v1.test'] + .post({ id: 1337, foo: 'test', bar: 42 }) + .set('x-send-unknown-status-code', 'true') + .decodeExpecting(200) + .then(() => '') + .catch((err) => (err instanceof DecodeError ? err.message : '')); - assert.isTrue(result); + assert.deepEqual(result, 'Unexpected response 400: {"error":"bad request"}'); }); it('throws for decode errors', async () => { @@ -180,10 +203,10 @@ describe('request', () => { .post({ id: 1337, foo: 'test', bar: 42 }) .set('x-send-invalid-response-body', 'true') .decodeExpecting(200) - .then(() => false) - .catch(() => true); + .then(() => '') + .catch((err) => (err instanceof DecodeError ? err.message : '')); - assert.isTrue(result); + assert.deepEqual(result, 'Could not decode response 200: {"invalid":"response"}'); }); }); @@ -210,8 +233,7 @@ describe('request', () => { .set('x-send-unexpected-status-code', 'true') .decode(); - assert.equal(req.status, 'decodeError'); - assert.equal(req.original.status, 400); + assert.equal(req.status, 401); }); }); });