From c6a9eaa257eb00d94f854d7cc0154dab5c051f2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20=C5=81=C4=85giewka?= Date: Sun, 22 Dec 2024 16:21:04 +0100 Subject: [PATCH] fix: parse JSON body when Content-Type has charset Or other parameters. In accordance with RFC 1341[^1], Content-Type contains first and foremost the media type. It can be followed by key-value pairs of parameters. This change supports extracting the media type and effectively discarding the other parameters as these are currently irrelevant to body parsing. The result media type is then matched against previously used JSON type tests. Closes #229 Closes #341 [^1]:https://www.rfc-editor.org/rfc/rfc1341 --- .changeset/unlucky-geese-give.md | 5 ++++ src/RESTDataSource.ts | 6 ++-- src/__tests__/RESTDataSource.test.ts | 45 ++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 .changeset/unlucky-geese-give.md diff --git a/.changeset/unlucky-geese-give.md b/.changeset/unlucky-geese-give.md new file mode 100644 index 0000000..fa62f6f --- /dev/null +++ b/.changeset/unlucky-geese-give.md @@ -0,0 +1,5 @@ +--- +'@apollo/datasource-rest': patch +--- + +Parse JSON body when Content-Type has charset diff --git a/src/RESTDataSource.ts b/src/RESTDataSource.ts index 9246f54..3babd86 100644 --- a/src/RESTDataSource.ts +++ b/src/RESTDataSource.ts @@ -303,14 +303,14 @@ export abstract class RESTDataSource { protected parseBody(response: FetcherResponse): Promise { const contentType = response.headers.get('Content-Type'); const contentLength = response.headers.get('Content-Length'); + const mediaType = contentType?.split(';')?.[0]; if ( // As one might expect, a "204 No Content" is empty! This means there // isn't enough to `JSON.parse`, and trying will result in an error. response.status !== 204 && contentLength !== '0' && - contentType && - (contentType.startsWith('application/json') || - contentType.endsWith('+json')) + (mediaType?.startsWith('application/json') || + mediaType?.endsWith('+json')) ) { return response.json(); } else { diff --git a/src/__tests__/RESTDataSource.test.ts b/src/__tests__/RESTDataSource.test.ts index f3e7cda..4652c29 100644 --- a/src/__tests__/RESTDataSource.test.ts +++ b/src/__tests__/RESTDataSource.test.ts @@ -784,6 +784,51 @@ describe('RESTDataSource', () => { expect(data).toEqual({ foo: 'bar' }); }); + it('returns data as parsed JSON when Content-Type ends in +json and has charset provided', async () => { + const dataSource = new (class extends RESTDataSource { + override baseURL = 'https://api.example.com'; + + getFoo() { + return this.get('foo'); + } + })(); + + nock(apiUrl) + .get('/foo') + .reply( + 200, + { foo: 'bar' }, + { 'content-type': 'application/vnd.api+json; charset=utf-8' }, + ); + + const data = await dataSource.getFoo(); + + expect(data).toEqual({ foo: 'bar' }); + }); + + it('returns data as parsed JSON when Content-Type ends in +json and has many parameters provided', async () => { + const dataSource = new (class extends RESTDataSource { + override baseURL = 'https://api.example.com'; + + getFoo() { + return this.get('foo'); + } + })(); + + nock(apiUrl).get('/foo').reply( + 200, + { foo: 'bar' }, + { + 'content-type': + 'application/vnd.api+json; charset=utf-8; boundary=ExampleBoundaryString', + }, + ); + + const data = await dataSource.getFoo(); + + expect(data).toEqual({ foo: 'bar' }); + }); + it('returns data as a string when Content-Type is text/plain', async () => { const dataSource = new (class extends RESTDataSource { override baseURL = 'https://api.example.com';