diff --git a/src/handler.ts b/src/handler.ts index 5dada52..fd82efe 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -572,23 +572,28 @@ export function createHandler< return async function handler(req) { let acceptedMediaType: AcceptableMediaType | null = null; + let acceptedMediaTypeQuality = 0; const accepts = (getHeader(req, 'accept') || '*/*') .replace(/\s/g, '') .toLowerCase() .split(','); for (const accept of accepts) { // accept-charset became obsolete, shouldnt be used (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Charset) - // TODO: handle the weight parameter "q" const [mediaType, ...params] = accept.split(';'); + const q = params.find((param) => param.startsWith('q=')); + const quality = q ? Number(q.substring(2)) : 1; + if (!Number.isFinite(quality) || quality <= 0 || quality > 1) { + continue; + } const charset = params?.find((param) => param.includes('charset=')) || 'charset=utf-8'; // utf-8 is assumed when not specified; + let acceptableMediaType: AcceptableMediaType | null = null; if ( mediaType === 'application/graphql-response+json' && charset === 'charset=utf-8' ) { - acceptedMediaType = 'application/graphql-response+json'; - break; + acceptableMediaType = 'application/graphql-response+json'; } // application/json should be the default until watershed @@ -598,8 +603,12 @@ export function createHandler< mediaType === '*/*') && (charset === 'charset=utf-8' || charset === 'charset=utf8') ) { - acceptedMediaType = 'application/json'; - break; + acceptableMediaType = 'application/json'; + } + + if (acceptableMediaType && quality > acceptedMediaTypeQuality) { + acceptedMediaType = acceptableMediaType; + acceptedMediaTypeQuality = quality; } } if (!acceptedMediaType) { diff --git a/tests/handler.test.ts b/tests/handler.test.ts index 0549599..e253bd4 100644 --- a/tests/handler.test.ts +++ b/tests/handler.test.ts @@ -457,3 +457,72 @@ it('should accept both utf-8 and utf8 charsets ', async () => { ] `); }); + +it('should reject media types with q=0', async () => { + const { request } = createTHandler(); + + await expect( + request( + 'GET', + { query: '{ __typename }' }, + { accept: 'application/json;q=0' }, + ), + ).resolves.toMatchInlineSnapshot(` + [ + null, + { + "headers": { + "accept": "application/graphql-response+json; charset=utf-8, application/json; charset=utf-8", + }, + "status": 406, + "statusText": "Not Acceptable", + }, + ] + `); +}); + +it('should prefer the accepted media type with the highest q value', async () => { + const { request } = createTHandler(); + + await expect( + request( + 'GET', + { query: '{ __typename }' }, + { + accept: 'application/json;q=0, application/graphql-response+json;q=1', + }, + ), + ).resolves.toMatchInlineSnapshot(` + [ + "{"data":{"__typename":"Query"}}", + { + "headers": { + "content-type": "application/graphql-response+json; charset=utf-8", + }, + "status": 200, + "statusText": "OK", + }, + ] + `); + + await expect( + request( + 'GET', + { query: '{ __typename }' }, + { + accept: 'application/graphql-response+json;q=0.1, application/json;q=1', + }, + ), + ).resolves.toMatchInlineSnapshot(` + [ + "{"data":{"__typename":"Query"}}", + { + "headers": { + "content-type": "application/json; charset=utf-8", + }, + "status": 200, + "statusText": "OK", + }, + ] + `); +});