-
Notifications
You must be signed in to change notification settings - Fork 22
feat: add decoded response to decodeExpecting errors #162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Route extends h.HttpRoute> = { | |
| }; | ||
| }[keyof Route['response']]; | ||
|
|
||
| type DecodedResponse<Route extends h.HttpRoute> = | ||
| export type DecodedResponse<Route extends h.HttpRoute> = | ||
| | SuccessfulResponses<Route> | ||
| | { | ||
| status: 'decodeError'; | ||
| error: unknown; | ||
| error: string; | ||
| body: unknown; | ||
| original: Response; | ||
| }; | ||
|
|
||
| export class DecodeError extends Error { | ||
| readonly decodedResponse: DecodedResponse<h.HttpRoute>; | ||
|
|
||
| constructor(message: string, decodedResponse: DecodedResponse<h.HttpRoute>) { | ||
| super(message); | ||
| this.decodedResponse = decodedResponse; | ||
| } | ||
| } | ||
|
|
||
| const decodedResponse = <Route extends h.HttpRoute>(res: DecodedResponse<Route>) => res; | ||
|
|
||
| type ExpectedDecodedResponse< | ||
|
|
@@ -134,7 +144,7 @@ const patchRequest = < | |
| // DISCUSS: what's this non-standard HTTP status code? | ||
| decodedResponse<Route>({ | ||
| 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<h.HttpRoute>); | ||
| } else if (res.status === 'decodeError') { | ||
| const error = `Could not decode response ${String( | ||
| res.original.status, | ||
| )}: ${JSON.stringify(res.original.body)}`; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that fp-ts provides a wrapper that returns an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hurm how would you want to handle this case, @bitgopatmcl? It's pretty gnarly I was about to open a ticket to track and then I had the thought -- is it even possible for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about if the server returns a non- |
||
| throw new DecodeError(error, res as DecodedResponse<h.HttpRoute>); | ||
| } else { | ||
| return res as ExpectedDecodedResponse<Route, StatusCode>; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,21 +181,32 @@ 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 () => { | ||
| const result = await apiClient['api.v1.test'] | ||
| .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"}'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| }); | ||
| }); | ||
|
|
||
|
|
@@ -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); | ||
| }); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is
res.original.statusnot already coerced to a string by the backticks?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#170