diff --git a/src/Client.spec.ts b/src/Client.spec.ts new file mode 100644 index 00000000..62d6f949 --- /dev/null +++ b/src/Client.spec.ts @@ -0,0 +1,51 @@ +import nock from 'nock' +import { Client } from './Client' + +const client = new Client({ token: 'mockToken' }) + +describe('Client', () => { + afterEach(() => { + nock.cleanAll() + }) + + test('should keep the status of a JSON error response', async () => { + nock(/(.*)/) + .get('/air/offers/off_123') + .reply(404, { + meta: { request_id: 'req_123', status: 404 }, + errors: [ + { + code: 'not_found', + title: 'Resource not found', + message: 'The resource you requested could not be found.', + type: 'invalid_request_error', + documentation_url: '', + }, + ], + }) + + await expect( + client.request({ method: 'GET', path: '/air/offers/off_123' }), + ).rejects.toMatchObject({ + status: 404, + meta: { request_id: 'req_123', status: 404 }, + }) + }) + + test('should keep the status when the error response is not JSON', async () => { + nock(/(.*)/) + .get('/air/offer_requests') + .reply(503, '503 Service Unavailable', { + 'content-type': 'text/html', + }) + + // A gateway responding on Duffel's behalf produces no `meta`, so `status` is the only + // indication of what went wrong. + await expect( + client.request({ method: 'GET', path: '/air/offer_requests' }), + ).rejects.toMatchObject({ + status: 503, + meta: undefined, + }) + }) +}) diff --git a/src/Client.ts b/src/Client.ts index a33b236a..58bfa05c 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -20,19 +20,36 @@ export class DuffelError extends Error { public errors: ApiResponseError[] public headers: Headers + /** + * The [HTTP status](https://httpstatuses.com/) the SDK received. + * + * `meta` is only populated when the API returned a JSON body, so this is the + * only status available when a proxy or gateway responds with something else. + * + * It is the status of the response that arrived, not necessarily the outcome + * of the request at Duffel: a gateway can time out or fail while Duffel still + * processes the request successfully. Treat it as diagnostic information, and + * do not use it on its own to decide whether the request took effect or + * whether it is safe to retry. + */ + public status: number | undefined + constructor({ meta, errors, headers, + status, }: { meta: ApiResponseMeta errors: ApiResponseError[] headers: Headers + status?: number }) { super() this.meta = meta this.errors = errors this.headers = headers + this.status = status ?? meta?.status } }