@@ -161,6 +161,31 @@ export type FormatError = (
161
161
err : Readonly < GraphQLError | Error > ,
162
162
) => GraphQLError | Error ;
163
163
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
+
164
189
/** @category Server */
165
190
export type OperationArgs < Context extends OperationContext = undefined > =
166
191
ExecutionArgs & { contextValue ?: Context } ;
@@ -326,6 +351,12 @@ export interface HandlerOptions<
326
351
* this formatter.
327
352
*/
328
353
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 > ;
329
360
}
330
361
331
362
/**
@@ -416,23 +447,10 @@ export function createHandler<
416
447
onSubscribe,
417
448
onOperation,
418
449
formatError = ( err ) => err ,
450
+ parseRequestParams = defaultParseRequestParams ,
419
451
} = options ;
420
452
421
453
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
-
436
454
let acceptedMediaType : AcceptableMediaType | null = null ;
437
455
const accepts = ( getHeader ( req , 'accept' ) || '*/*' )
438
456
. replace ( / \s / g, '' )
@@ -478,96 +496,12 @@ export function createHandler<
478
496
] ;
479
497
}
480
498
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 ;
492
500
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 ;
571
505
} catch ( err ) {
572
506
return makeResponse ( err , acceptedMediaType , formatError ) ;
573
507
}
@@ -653,7 +587,7 @@ export function createHandler<
653
587
654
588
// mutations cannot happen over GETs
655
589
// https://graphql.github.io/graphql-over-http/draft/#sel-CALFJRPAAELBAAxwP
656
- if ( operation === 'mutation' && method === 'GET' ) {
590
+ if ( operation === 'mutation' && req . method === 'GET' ) {
657
591
return [
658
592
JSON . stringify ( {
659
593
errors : [ new GraphQLError ( 'Cannot perform mutations over GET' ) ] ,
@@ -701,6 +635,116 @@ type AcceptableMediaType =
701
635
| 'application/graphql-response+json'
702
636
| 'application/json' ;
703
637
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
+
704
748
/**
705
749
* Creates an appropriate GraphQL over HTTP response following the provided arguments.
706
750
*
0 commit comments