From 3eb471bf00d874007b8b0ad1d16e7f348ac1ea54 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 | 47 ++++++++++++++++++++++++++++ 3 files changed, 55 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..a55443f 100644 --- a/src/__tests__/RESTDataSource.test.ts +++ b/src/__tests__/RESTDataSource.test.ts @@ -784,6 +784,53 @@ 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';