Skip to content

Commit 99a192e

Browse files
authored
feat!: add spec compliant default Accept header (#618)
1 parent 0e53aed commit 99a192e

File tree

6 files changed

+60
-121
lines changed

6 files changed

+60
-121
lines changed

src/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const ACCEPT_HEADER = `Accept`
2+
export const CONTENT_TYPE_HEADER = `Content-Type`
3+
export const CONTENT_TYPE_JSON = `application/json`
4+
export const CONTENT_TYPE_GQL = `application/graphql-response+json`

src/index.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ACCEPT_HEADER, CONTENT_TYPE_GQL, CONTENT_TYPE_HEADER, CONTENT_TYPE_JSON } from './constants.js'
12
import { defaultJsonSerializer } from './defaultJsonSerializer.js'
23
import { HeadersInstanceToPlainObject, uppercase } from './helpers.js'
34
import {
@@ -133,14 +134,18 @@ const createHttpMethodFetcher =
133134
async <V extends Variables>(params: RequestVerbParams<V>) => {
134135
const { url, query, variables, operationName, fetch, fetchOptions, middleware } = params
135136

136-
const headers = new Headers(params.headers as HeadersInit)
137+
const headers = new Headers(params.headers)
137138
let queryParams = ``
138139
let body = undefined
139140

141+
if (!headers.has(ACCEPT_HEADER)) {
142+
headers.set(ACCEPT_HEADER, [CONTENT_TYPE_GQL, CONTENT_TYPE_JSON].join(`, `))
143+
}
144+
140145
if (method === `POST`) {
141146
body = createRequestBody(query, variables, operationName, fetchOptions.jsonSerializer)
142-
if (typeof body === `string` && !headers.has(`Content-Type`)) {
143-
headers.set(`Content-Type`, `application/json`)
147+
if (typeof body === `string` && !headers.has(CONTENT_TYPE_HEADER)) {
148+
headers.set(CONTENT_TYPE_HEADER, CONTENT_TYPE_JSON)
144149
}
145150
} else {
146151
// @ts-expect-error todo needs ADT for TS to understand the different states
@@ -619,26 +624,21 @@ const getResult = async (
619624
| { data: undefined; errors: object }
620625
| { data: undefined; errors: object[] }
621626
> => {
622-
let contentType: string | undefined
623-
624-
response.headers.forEach((value, key) => {
625-
if (key.toLowerCase() === `content-type`) {
626-
contentType = value
627-
}
628-
})
627+
const contentType = response.headers.get(CONTENT_TYPE_HEADER)
629628

630-
if (
631-
contentType &&
632-
(contentType.toLowerCase().startsWith(`application/json`) ||
633-
contentType.toLowerCase().startsWith(`application/graphql+json`) ||
634-
contentType.toLowerCase().startsWith(`application/graphql-response+json`))
635-
) {
629+
if (contentType && isJsonContentType(contentType)) {
636630
return jsonSerializer.parse(await response.text()) as any
637631
} else {
638632
return response.text() as any
639633
}
640634
}
641635

636+
const isJsonContentType = (contentType: string) => {
637+
const contentTypeLower = contentType.toLowerCase()
638+
639+
return contentTypeLower.includes(CONTENT_TYPE_GQL) || contentTypeLower.includes(CONTENT_TYPE_JSON)
640+
}
641+
642642
const callOrIdentity = <T>(value: MaybeLazy<T>) => {
643643
return typeof value === `function` ? (value as () => T)() : value
644644
}

tests/__snapshots__/document-node.test.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ exports[`accepts graphql DocumentNode as alternative to raw string 1`] = `
1212
}",
1313
},
1414
"headers": {
15-
"accept": "*/*",
15+
"accept": "application/graphql-response+json, application/json",
1616
"accept-encoding": "gzip, deflate",
1717
"accept-language": "*",
1818
"connection": "keep-alive",

tests/__snapshots__/gql.test.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ exports[`gql > passthrough allowing benefits of tooling for gql template tag 1`]
1111
}",
1212
},
1313
"headers": {
14-
"accept": "*/*",
14+
"accept": "application/graphql-response+json, application/json",
1515
"accept-encoding": "gzip, deflate",
1616
"accept-language": "*",
1717
"connection": "keep-alive",

tests/general.test.ts

Lines changed: 38 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { GraphQLClient, rawRequest, request } from '../src/index.js'
2-
import type { RequestConfig } from '../src/types.js'
32
import { setupMockServer } from './__helpers.js'
43
import { gql } from 'graphql-tag'
54
import type { Mock } from 'vitest'
@@ -62,65 +61,6 @@ test(`minimal raw query with response headers`, async () => {
6261
expect(headers.get(`X-Custom-Header`)).toEqual(reqHeaders![`X-Custom-Header`])
6362
})
6463

65-
test(`minimal raw query with response headers and new graphql content type`, async () => {
66-
const { headers: _, body } = ctx.res({
67-
headers: {
68-
'Content-Type': `application/graphql+json`,
69-
},
70-
body: {
71-
data: {
72-
me: {
73-
id: `some-id`,
74-
},
75-
},
76-
extensions: {
77-
version: `1`,
78-
},
79-
},
80-
}).spec
81-
82-
const { headers: __, ...result } = await rawRequest(ctx.url, `{ me { id } }`)
83-
84-
expect(result).toEqual({ ...body, status: 200 })
85-
})
86-
87-
test(`minimal raw query with response headers and application/graphql-response+json response type`, async () => {
88-
const { headers: _, body } = ctx.res({
89-
headers: {
90-
'Content-Type': `application/graphql-response+json`,
91-
},
92-
body: {
93-
data: {
94-
me: {
95-
id: `some-id`,
96-
},
97-
},
98-
extensions: {
99-
version: `1`,
100-
},
101-
},
102-
}).spec
103-
104-
const { headers: __, ...result } = await rawRequest(ctx.url, `{ me { id } }`)
105-
106-
expect(result).toEqual({ ...body, status: 200 })
107-
})
108-
109-
test(`content-type with charset`, async () => {
110-
const { data } = ctx.res({
111-
// headers: { 'Content-Type': 'application/json; charset=utf-8' },
112-
body: {
113-
data: {
114-
me: {
115-
id: `some-id`,
116-
},
117-
},
118-
},
119-
}).spec.body!
120-
121-
expect(await request(ctx.url, `{ me { id } }`)).toEqual(data)
122-
})
123-
12464
test(`basic error`, async () => {
12565
ctx.res({
12666
body: {
@@ -336,31 +276,6 @@ test.skip(`extra fetch options`, async () => {
336276
`)
337277
})
338278

339-
test(`case-insensitive content-type header for custom fetch`, async () => {
340-
const testData = { data: { test: `test` } }
341-
const testResponseHeaders = new Map()
342-
testResponseHeaders.set(`ConTENT-type`, `apPliCatiON/JSON`)
343-
344-
const options: RequestConfig = {
345-
// @ts-expect-error testing
346-
fetch: (url) =>
347-
Promise.resolve({
348-
headers: testResponseHeaders,
349-
data: testData,
350-
json: () => testData,
351-
text: () => JSON.stringify(testData),
352-
ok: true,
353-
status: 200,
354-
url,
355-
}),
356-
}
357-
358-
const client = new GraphQLClient(ctx.url, options)
359-
const result = await client.request(`{ test }`)
360-
361-
expect(result).toEqual(testData.data)
362-
})
363-
364279
describe(`operationName parsing`, () => {
365280
it(`should work for gql documents`, async () => {
366281
const mock = ctx.res({ body: { data: { foo: 1 } } })
@@ -405,3 +320,41 @@ test(`should not throw error when errors property is an empty array (occurred wh
405320

406321
expect(res).toEqual(expect.objectContaining({ test: `test` }))
407322
})
323+
324+
it(`adds the default headers to the request`, async () => {
325+
const mock = ctx.res({ body: { data: {} } })
326+
await request(
327+
ctx.url,
328+
gql`
329+
query myGqlOperation {
330+
users
331+
}
332+
`,
333+
)
334+
335+
const headers = mock.requests[0]?.headers
336+
expect(headers?.[`accept`]).toEqual(`application/graphql-response+json, application/json`)
337+
expect(headers?.[`content-type`]).toEqual(`application/json`)
338+
})
339+
340+
it(`allows overriding the default headers for the request`, async () => {
341+
const mock = ctx.res({ body: { data: {} } })
342+
const query = gql`
343+
query myGqlOperation {
344+
users
345+
}
346+
`
347+
348+
await request({
349+
url: ctx.url,
350+
document: query,
351+
requestHeaders: {
352+
accept: `text/plain`,
353+
'content-type': `text/plain`,
354+
},
355+
})
356+
357+
const headers = mock.requests[0]?.headers
358+
expect(headers?.[`accept`]).toEqual(`text/plain`)
359+
expect(headers?.[`content-type`]).toEqual(`text/plain`)
360+
})

tests/headers.test.ts

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -107,24 +107,6 @@ describe(`using class`, () => {
107107
expect(mock.requests[0]?.headers[`x-foo`]).toEqual(`new`)
108108
})
109109
})
110-
111-
describe(`allows content-type header to be overwritten`, () => {
112-
test(`with request method`, async () => {
113-
const headers = new Headers({ 'content-type': `text/plain` })
114-
const client = new GraphQLClient(ctx.url, { headers })
115-
const mock = ctx.res()
116-
await client.request(`{ me { id } }`)
117-
expect(mock.requests[0]?.headers[`content-type`]).toEqual(`text/plain`)
118-
})
119-
120-
test(`with rawRequest method`, async () => {
121-
const headers = new Headers({ 'content-type': `text/plain` })
122-
const client = new GraphQLClient(ctx.url, { headers })
123-
const mock = ctx.res()
124-
await client.rawRequest(`{ me { id } }`)
125-
expect(mock.requests[0]?.headers[`content-type`]).toEqual(`text/plain`)
126-
})
127-
})
128110
})
129111
})
130112

0 commit comments

Comments
 (0)