Skip to content

Commit 7e7dc48

Browse files
committed
implement
1 parent c1b85b3 commit 7e7dc48

File tree

1 file changed

+148
-104
lines changed

1 file changed

+148
-104
lines changed

src/handler.ts

+148-104
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,31 @@ export type FormatError = (
161161
err: Readonly<GraphQLError | Error>,
162162
) => GraphQLError | Error;
163163

164+
/**
165+
* The request parser for an incoming GraphQL request. It parses and validates the
166+
* request itself, including the request method and the content-type of the body.
167+
*
168+
* In case you are extending the server to handle more request types, this is the
169+
* perfect place to do so.
170+
*
171+
* If an error is thrown, it will be formatted using the provided {@link FormatError}
172+
* and handled following the spec to be gracefully reported to the client.
173+
*
174+
* Throwing an instance of `Error` will _always_ have the client respond with a `400: Bad Request`
175+
* and the error's message in the response body; however, if an instance of `GraphQLError` is thrown,
176+
* it will be reported depending on the accepted content-type.
177+
*
178+
* If you return nothing, the default parser will be used instead.
179+
*
180+
* @category Server
181+
*/
182+
export type ParseRequestParams<
183+
RequestRaw = unknown,
184+
RequestContext = unknown,
185+
> = (
186+
req: Request<RequestRaw, RequestContext>,
187+
) => Promise<RequestParams | Response | void> | RequestParams | Response | void;
188+
164189
/** @category Server */
165190
export type OperationArgs<Context extends OperationContext = undefined> =
166191
ExecutionArgs & { contextValue?: Context };
@@ -326,6 +351,12 @@ export interface HandlerOptions<
326351
* this formatter.
327352
*/
328353
formatError?: FormatError;
354+
/**
355+
* The request parser for an incoming GraphQL request.
356+
*
357+
* Read more about it in {@link ParseRequestParams}.
358+
*/
359+
parseRequestParams?: ParseRequestParams<RequestRaw, RequestContext>;
329360
}
330361

331362
/**
@@ -416,23 +447,10 @@ export function createHandler<
416447
onSubscribe,
417448
onOperation,
418449
formatError = (err) => err,
450+
parseRequestParams = defaultParseRequestParams,
419451
} = options;
420452

421453
return async function handler(req) {
422-
const method = req.method;
423-
if (method !== 'GET' && method !== 'POST') {
424-
return [
425-
null,
426-
{
427-
status: 405,
428-
statusText: 'Method Not Allowed',
429-
headers: {
430-
allow: 'GET, POST',
431-
},
432-
},
433-
];
434-
}
435-
436454
let acceptedMediaType: AcceptableMediaType | null = null;
437455
const accepts = (getHeader(req, 'accept') || '*/*')
438456
.replace(/\s/g, '')
@@ -478,96 +496,12 @@ export function createHandler<
478496
];
479497
}
480498

481-
// TODO: should graphql-http care about content-encoding? I'd say unzipping should happen before handler is reached
482-
483-
const [
484-
mediaType,
485-
charset = 'charset=utf-8', // utf-8 is assumed when not specified. this parameter is either "charset" or "boundary" (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length)
486-
] = (getHeader(req, 'content-type') || '')
487-
.replace(/\s/g, '')
488-
.toLowerCase()
489-
.split(';');
490-
491-
let params;
499+
let params: RequestParams;
492500
try {
493-
const partParams: Partial<RequestParams> = {};
494-
switch (true) {
495-
case method === 'GET': {
496-
// TODO: what if content-type is specified and is not application/x-www-form-urlencoded?
497-
try {
498-
const [, search] = req.url.split('?');
499-
const searchParams = new URLSearchParams(search);
500-
partParams.operationName =
501-
searchParams.get('operationName') ?? undefined;
502-
partParams.query = searchParams.get('query') ?? undefined;
503-
const variables = searchParams.get('variables');
504-
if (variables) partParams.variables = JSON.parse(variables);
505-
const extensions = searchParams.get('extensions');
506-
if (extensions) partParams.extensions = JSON.parse(extensions);
507-
} catch {
508-
throw new Error('Unparsable URL');
509-
}
510-
break;
511-
}
512-
case method === 'POST' &&
513-
mediaType === 'application/json' &&
514-
charset === 'charset=utf-8': {
515-
if (!req.body) {
516-
throw new Error('Missing body');
517-
}
518-
let data;
519-
try {
520-
const body =
521-
typeof req.body === 'function' ? await req.body() : req.body;
522-
data = typeof body === 'string' ? JSON.parse(body) : body;
523-
} catch (err) {
524-
throw new Error('Unparsable JSON body');
525-
}
526-
if (!isObject(data)) {
527-
throw new Error('JSON body must be an object');
528-
}
529-
partParams.operationName = data.operationName;
530-
partParams.query = data.query;
531-
partParams.variables = data.variables;
532-
partParams.extensions = data.extensions;
533-
break;
534-
}
535-
default: // graphql-http doesnt support any other content type
536-
return [
537-
null,
538-
{
539-
status: 415,
540-
statusText: 'Unsupported Media Type',
541-
},
542-
];
543-
}
544-
545-
if (partParams.query == null) throw new Error('Missing query');
546-
if (typeof partParams.query !== 'string')
547-
throw new Error('Invalid query');
548-
if (
549-
partParams.variables != null &&
550-
(typeof partParams.variables !== 'object' ||
551-
Array.isArray(partParams.variables))
552-
) {
553-
throw new Error('Invalid variables');
554-
}
555-
if (
556-
partParams.operationName != null &&
557-
typeof partParams.operationName !== 'string'
558-
) {
559-
throw new Error('Invalid operationName');
560-
}
561-
if (
562-
partParams.extensions != null &&
563-
(typeof partParams.extensions !== 'object' ||
564-
Array.isArray(partParams.extensions))
565-
) {
566-
throw new Error('Invalid extensions');
567-
}
568-
569-
// request parameters are checked and now complete
570-
params = partParams as RequestParams;
501+
let paramsOrRes = await parseRequestParams(req);
502+
if (!paramsOrRes) paramsOrRes = await defaultParseRequestParams(req);
503+
if (isResponse(paramsOrRes)) return paramsOrRes;
504+
params = paramsOrRes;
571505
} catch (err) {
572506
return makeResponse(err, acceptedMediaType, formatError);
573507
}
@@ -653,7 +587,7 @@ export function createHandler<
653587

654588
// mutations cannot happen over GETs
655589
// https://graphql.github.io/graphql-over-http/draft/#sel-CALFJRPAAELBAAxwP
656-
if (operation === 'mutation' && method === 'GET') {
590+
if (operation === 'mutation' && req.method === 'GET') {
657591
return [
658592
JSON.stringify({
659593
errors: [new GraphQLError('Cannot perform mutations over GET')],
@@ -701,6 +635,116 @@ type AcceptableMediaType =
701635
| 'application/graphql-response+json'
702636
| 'application/json';
703637

638+
/**
639+
* The default request params parser. Used when no custom one is provided or if it
640+
* returns nothing.
641+
*
642+
* TODO: should graphql-http itself care about content-encoding? I'd say unzipping should happen before handler is reached
643+
*/
644+
async function defaultParseRequestParams(
645+
req: Request<unknown, unknown>,
646+
): Promise<Response | RequestParams> {
647+
const method = req.method;
648+
if (method !== 'GET' && method !== 'POST') {
649+
return [
650+
null,
651+
{
652+
status: 405,
653+
statusText: 'Method Not Allowed',
654+
headers: {
655+
allow: 'GET, POST',
656+
},
657+
},
658+
];
659+
}
660+
661+
const [
662+
mediaType,
663+
charset = 'charset=utf-8', // utf-8 is assumed when not specified. this parameter is either "charset" or "boundary" (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length)
664+
] = (getHeader(req, 'content-type') || '')
665+
.replace(/\s/g, '')
666+
.toLowerCase()
667+
.split(';');
668+
669+
const partParams: Partial<RequestParams> = {};
670+
switch (true) {
671+
case method === 'GET': {
672+
// TODO: what if content-type is specified and is not application/x-www-form-urlencoded?
673+
try {
674+
const [, search] = req.url.split('?');
675+
const searchParams = new URLSearchParams(search);
676+
partParams.operationName =
677+
searchParams.get('operationName') ?? undefined;
678+
partParams.query = searchParams.get('query') ?? undefined;
679+
const variables = searchParams.get('variables');
680+
if (variables) partParams.variables = JSON.parse(variables);
681+
const extensions = searchParams.get('extensions');
682+
if (extensions) partParams.extensions = JSON.parse(extensions);
683+
} catch {
684+
throw new Error('Unparsable URL');
685+
}
686+
break;
687+
}
688+
case method === 'POST' &&
689+
mediaType === 'application/json' &&
690+
charset === 'charset=utf-8': {
691+
if (!req.body) {
692+
throw new Error('Missing body');
693+
}
694+
let data;
695+
try {
696+
const body =
697+
typeof req.body === 'function' ? await req.body() : req.body;
698+
data = typeof body === 'string' ? JSON.parse(body) : body;
699+
} catch (err) {
700+
throw new Error('Unparsable JSON body');
701+
}
702+
if (!isObject(data)) {
703+
throw new Error('JSON body must be an object');
704+
}
705+
partParams.operationName = data.operationName;
706+
partParams.query = data.query;
707+
partParams.variables = data.variables;
708+
partParams.extensions = data.extensions;
709+
break;
710+
}
711+
default: // graphql-http doesnt support any other content type
712+
return [
713+
null,
714+
{
715+
status: 415,
716+
statusText: 'Unsupported Media Type',
717+
},
718+
];
719+
}
720+
721+
if (partParams.query == null) throw new Error('Missing query');
722+
if (typeof partParams.query !== 'string') throw new Error('Invalid query');
723+
if (
724+
partParams.variables != null &&
725+
(typeof partParams.variables !== 'object' ||
726+
Array.isArray(partParams.variables))
727+
) {
728+
throw new Error('Invalid variables');
729+
}
730+
if (
731+
partParams.operationName != null &&
732+
typeof partParams.operationName !== 'string'
733+
) {
734+
throw new Error('Invalid operationName');
735+
}
736+
if (
737+
partParams.extensions != null &&
738+
(typeof partParams.extensions !== 'object' ||
739+
Array.isArray(partParams.extensions))
740+
) {
741+
throw new Error('Invalid extensions');
742+
}
743+
744+
// request parameters are checked and now complete
745+
return partParams as RequestParams;
746+
}
747+
704748
/**
705749
* Creates an appropriate GraphQL over HTTP response following the provided arguments.
706750
*

0 commit comments

Comments
 (0)