diff --git a/.changeset/empty-yaks-juggle.md b/.changeset/empty-yaks-juggle.md new file mode 100644 index 0000000..21e76ca --- /dev/null +++ b/.changeset/empty-yaks-juggle.md @@ -0,0 +1,5 @@ +--- +'@apollo/datasource-rest': major +--- + +The `errorFromResponse` method now receives an options object with `url`, `request`, `response`, and `parsedBody` rather than just a response, and the body has already been parsed. diff --git a/.changeset/silly-apricots-exercise.md b/.changeset/silly-apricots-exercise.md new file mode 100644 index 0000000..8ce9c52 --- /dev/null +++ b/.changeset/silly-apricots-exercise.md @@ -0,0 +1,5 @@ +--- +'@apollo/datasource-rest': minor +--- + +New `throwIfResponseIsError` hook allows you to control whether a response should be returned or thrown as an error. Partially replaces the removed `didReceiveResponse` hook. diff --git a/src/RESTDataSource.ts b/src/RESTDataSource.ts index 9673f9b..e3a3faa 100644 --- a/src/RESTDataSource.ts +++ b/src/RESTDataSource.ts @@ -250,9 +250,27 @@ export abstract class RESTDataSource { ); } - protected async errorFromResponse(response: FetcherResponse) { - const body = await this.parseBody(response); + protected async throwIfResponseIsError(options: { + url: URL; + request: RequestOptions; + response: FetcherResponse; + parsedBody: unknown; + }) { + if (options.response.ok) { + return; + } + throw await this.errorFromResponse(options); + } + protected async errorFromResponse({ + response, + parsedBody, + }: { + url: URL; + request: RequestOptions; + response: FetcherResponse; + parsedBody: unknown; + }) { return new GraphQLError(`${response.status}: ${response.statusText}`, { extensions: { ...(response.status === 401 @@ -264,7 +282,7 @@ export abstract class RESTDataSource { url: response.url, status: response.status, statusText: response.statusText, - body, + body: parsedBody, }, }, }); @@ -372,11 +390,16 @@ export abstract class RESTDataSource { outgoingRequest.httpCacheSemanticsCachePolicyOptions, }); - if (response.ok) { - return (await this.parseBody(response)) as TResult; - } else { - throw await this.errorFromResponse(response); - } + const parsedBody = await this.parseBody(response); + + await this.throwIfResponseIsError({ + url, + request: outgoingRequest, + response, + parsedBody, + }); + + return parsedBody as TResult; } catch (error) { this.didEncounterError(error as Error, outgoingRequest); throw error; diff --git a/src/__tests__/RESTDataSource.test.ts b/src/__tests__/RESTDataSource.test.ts index eb383cf..c144ed5 100644 --- a/src/__tests__/RESTDataSource.test.ts +++ b/src/__tests__/RESTDataSource.test.ts @@ -839,6 +839,35 @@ describe('RESTDataSource', () => { }); describe('error handling', () => { + it('can throw on 200 with throwIfResponseIsError', async () => { + const dataSource = new (class extends RESTDataSource { + override baseURL = 'https://api.example.com'; + + getFoo() { + return this.get('foo'); + } + + protected override async throwIfResponseIsError( + options: Parameters[0], + ): Promise { + throw await this.errorFromResponse(options); + } + })(); + + nock(apiUrl).get('/foo').reply(200, 'Invalid token'); + + const result = dataSource.getFoo(); + await expect(result).rejects.toThrow(GraphQLError); + await expect(result).rejects.toMatchObject({ + extensions: { + response: { + status: 200, + body: 'Invalid token', + }, + }, + }); + }); + it('throws an UNAUTHENTICATED error when the response status is 401', async () => { const dataSource = new (class extends RESTDataSource { override baseURL = 'https://api.example.com';