From 704d636946a9a3232f31e69008a5a5c8aefe4698 Mon Sep 17 00:00:00 2001 From: Jon Koops Date: Sun, 5 Nov 2023 18:36:41 +0100 Subject: [PATCH] feat!: add spec compliant default `Accept` header --- src/constants.ts | 4 + src/index.ts | 35 ++--- .../__snapshots__/document-node.test.ts.snap | 2 +- tests/__snapshots__/gql.test.ts.snap | 2 +- tests/general.test.ts | 124 ++++++------------ tests/headers.test.ts | 18 --- 6 files changed, 64 insertions(+), 121 deletions(-) create mode 100644 src/constants.ts diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 000000000..c561ca772 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,4 @@ +export const ACCEPT_HEADER = `Accept` +export const CONTENT_TYPE_HEADER = `Content-Type`; +export const CONTENT_TYPE_JSON = `application/json`; +export const CONTENT_TYPE_GQL = `application/graphql-response+json`; diff --git a/src/index.ts b/src/index.ts index e68dad7c5..76ba822cd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +import { ACCEPT_HEADER, CONTENT_TYPE_GQL, CONTENT_TYPE_HEADER, CONTENT_TYPE_JSON } from './constants.js' import { defaultJsonSerializer } from './defaultJsonSerializer.js' import { HeadersInstanceToPlainObject, uppercase } from './helpers.js' import { @@ -133,14 +134,18 @@ const createHttpMethodFetcher = async (params: RequestVerbParams) => { const { url, query, variables, operationName, fetch, fetchOptions, middleware } = params - const headers = new Headers(params.headers as HeadersInit) + const headers = new Headers(params.headers) let queryParams = `` let body = undefined + if (!headers.has(ACCEPT_HEADER)) { + headers.set(ACCEPT_HEADER, [CONTENT_TYPE_GQL, CONTENT_TYPE_JSON].join(`, `)) + } + if (method === `POST`) { body = createRequestBody(query, variables, operationName, fetchOptions.jsonSerializer) - if (typeof body === `string` && !headers.has(`Content-Type`)) { - headers.set(`Content-Type`, `application/json`) + if (typeof body === `string` && !headers.has(CONTENT_TYPE_HEADER)) { + headers.set(CONTENT_TYPE_HEADER, CONTENT_TYPE_JSON) } } else { // @ts-expect-error todo needs ADT for TS to understand the different states @@ -619,26 +624,24 @@ const getResult = async ( | { data: undefined; errors: object } | { data: undefined; errors: object[] } > => { - let contentType: string | undefined + const contentType = response.headers.get(CONTENT_TYPE_HEADER) - response.headers.forEach((value, key) => { - if (key.toLowerCase() === `content-type`) { - contentType = value - } - }) - - if ( - contentType && - (contentType.toLowerCase().startsWith(`application/json`) || - contentType.toLowerCase().startsWith(`application/graphql+json`) || - contentType.toLowerCase().startsWith(`application/graphql-response+json`)) - ) { + if (contentType && isJsonContentType(contentType)) { return jsonSerializer.parse(await response.text()) as any } else { return response.text() as any } } +const isJsonContentType = (contentType: string) => { + const contentTypeLower = contentType.toLowerCase(); + + return ( + contentTypeLower.includes(CONTENT_TYPE_GQL) || + contentTypeLower.includes(CONTENT_TYPE_JSON) + ); +} + const callOrIdentity = (value: MaybeLazy) => { return typeof value === `function` ? (value as () => T)() : value } diff --git a/tests/__snapshots__/document-node.test.ts.snap b/tests/__snapshots__/document-node.test.ts.snap index 9fd89772c..5797c9460 100644 --- a/tests/__snapshots__/document-node.test.ts.snap +++ b/tests/__snapshots__/document-node.test.ts.snap @@ -12,7 +12,7 @@ exports[`accepts graphql DocumentNode as alternative to raw string 1`] = ` }", }, "headers": { - "accept": "*/*", + "accept": "application/graphql-response+json, application/json", "accept-encoding": "gzip, deflate", "accept-language": "*", "connection": "keep-alive", diff --git a/tests/__snapshots__/gql.test.ts.snap b/tests/__snapshots__/gql.test.ts.snap index f22869ee2..ba5b00222 100644 --- a/tests/__snapshots__/gql.test.ts.snap +++ b/tests/__snapshots__/gql.test.ts.snap @@ -11,7 +11,7 @@ exports[`gql > passthrough allowing benefits of tooling for gql template tag 1`] }", }, "headers": { - "accept": "*/*", + "accept": "application/graphql-response+json, application/json", "accept-encoding": "gzip, deflate", "accept-language": "*", "connection": "keep-alive", diff --git a/tests/general.test.ts b/tests/general.test.ts index a3520a954..6b300c4da 100644 --- a/tests/general.test.ts +++ b/tests/general.test.ts @@ -1,5 +1,4 @@ import { GraphQLClient, rawRequest, request } from '../src/index.js' -import type { RequestConfig } from '../src/types.js' import { setupMockServer } from './__helpers.js' import { gql } from 'graphql-tag' import type { Mock } from 'vitest' @@ -62,65 +61,6 @@ test(`minimal raw query with response headers`, async () => { expect(headers.get(`X-Custom-Header`)).toEqual(reqHeaders![`X-Custom-Header`]) }) -test(`minimal raw query with response headers and new graphql content type`, async () => { - const { headers: _, body } = ctx.res({ - headers: { - 'Content-Type': `application/graphql+json`, - }, - body: { - data: { - me: { - id: `some-id`, - }, - }, - extensions: { - version: `1`, - }, - }, - }).spec - - const { headers: __, ...result } = await rawRequest(ctx.url, `{ me { id } }`) - - expect(result).toEqual({ ...body, status: 200 }) -}) - -test(`minimal raw query with response headers and application/graphql-response+json response type`, async () => { - const { headers: _, body } = ctx.res({ - headers: { - 'Content-Type': `application/graphql-response+json`, - }, - body: { - data: { - me: { - id: `some-id`, - }, - }, - extensions: { - version: `1`, - }, - }, - }).spec - - const { headers: __, ...result } = await rawRequest(ctx.url, `{ me { id } }`) - - expect(result).toEqual({ ...body, status: 200 }) -}) - -test(`content-type with charset`, async () => { - const { data } = ctx.res({ - // headers: { 'Content-Type': 'application/json; charset=utf-8' }, - body: { - data: { - me: { - id: `some-id`, - }, - }, - }, - }).spec.body! - - expect(await request(ctx.url, `{ me { id } }`)).toEqual(data) -}) - test(`basic error`, async () => { ctx.res({ body: { @@ -336,31 +276,6 @@ test.skip(`extra fetch options`, async () => { `) }) -test(`case-insensitive content-type header for custom fetch`, async () => { - const testData = { data: { test: `test` } } - const testResponseHeaders = new Map() - testResponseHeaders.set(`ConTENT-type`, `apPliCatiON/JSON`) - - const options: RequestConfig = { - // @ts-expect-error testing - fetch: (url) => - Promise.resolve({ - headers: testResponseHeaders, - data: testData, - json: () => testData, - text: () => JSON.stringify(testData), - ok: true, - status: 200, - url, - }), - } - - const client = new GraphQLClient(ctx.url, options) - const result = await client.request(`{ test }`) - - expect(result).toEqual(testData.data) -}) - describe(`operationName parsing`, () => { it(`should work for gql documents`, async () => { const mock = ctx.res({ body: { data: { foo: 1 } } }) @@ -405,3 +320,42 @@ test(`should not throw error when errors property is an empty array (occurred wh expect(res).toEqual(expect.objectContaining({ test: `test` })) }) + + +it(`adds the default headers to the request`, async () => { + const mock = ctx.res({ body: { data: {} } }) + await request( + ctx.url, + gql` + query myGqlOperation { + users + } + `, + ) + + const headers = mock.requests[0]?.headers + expect(headers?.[`accept`]).toEqual(`application/graphql-response+json, application/json`) + expect(headers?.[`content-type`]).toEqual(`application/json`) +}) + +it(`allows overriding the default headers for the request`, async () => { + const mock = ctx.res({ body: { data: {} } }) + const query = gql` + query myGqlOperation { + users + } + ` + + await request({ + url: ctx.url, + document: query, + requestHeaders: { + 'accept': `text/plain`, + 'content-type': `text/plain`, + } + }) + + const headers = mock.requests[0]?.headers + expect(headers?.[`accept`]).toEqual(`text/plain`) + expect(headers?.[`content-type`]).toEqual(`text/plain`) +}) \ No newline at end of file diff --git a/tests/headers.test.ts b/tests/headers.test.ts index c297084ad..473af88a3 100644 --- a/tests/headers.test.ts +++ b/tests/headers.test.ts @@ -107,24 +107,6 @@ describe(`using class`, () => { expect(mock.requests[0]?.headers[`x-foo`]).toEqual(`new`) }) }) - - describe(`allows content-type header to be overwritten`, () => { - test(`with request method`, async () => { - const headers = new Headers({ 'content-type': `text/plain` }) - const client = new GraphQLClient(ctx.url, { headers }) - const mock = ctx.res() - await client.request(`{ me { id } }`) - expect(mock.requests[0]?.headers[`content-type`]).toEqual(`text/plain`) - }) - - test(`with rawRequest method`, async () => { - const headers = new Headers({ 'content-type': `text/plain` }) - const client = new GraphQLClient(ctx.url, { headers }) - const mock = ctx.res() - await client.rawRequest(`{ me { id } }`) - expect(mock.requests[0]?.headers[`content-type`]).toEqual(`text/plain`) - }) - }) }) })