@@ -19,7 +19,10 @@ use crate::layers::ServiceBuilderExt;
1919use crate :: plugin:: Plugin ;
2020use crate :: plugin:: PluginInit ;
2121use crate :: plugins:: limits:: layer:: BodyLimitError ;
22+ use crate :: plugins:: limits:: layer:: HeaderLimitError ;
2223use crate :: plugins:: limits:: layer:: RequestBodyLimitLayer ;
24+ use crate :: plugins:: limits:: layer:: RequestHeaderCountLimitLayer ;
25+ use crate :: plugins:: limits:: layer:: RequestHeaderListItemsLimitLayer ;
2326use crate :: services:: router;
2427use crate :: services:: router:: BoxService ;
2528
@@ -116,6 +119,20 @@ pub(crate) struct Config {
116119 #[ schemars( with = "Option<String>" , default ) ]
117120 pub ( crate ) http1_max_request_buf_size : Option < ByteSize > ,
118121
122+ /// Limit the maximum number of headers in an HTTP request.
123+ ///
124+ /// If router receives more headers than this limit, it responds to the client with
125+ /// "431 Request Header Fields Too Large".
126+ /// When not specified, no limit is enforced at the middleware level.
127+ pub ( crate ) http_max_request_headers : Option < usize > ,
128+
129+ /// Limit the maximum number of items in a header list (for headers with multiple values).
130+ ///
131+ /// If a single header has more values than this limit, it responds to the client with
132+ /// "431 Request Header Fields Too Large".
133+ /// When not specified, no limit is enforced at the middleware level.
134+ pub ( crate ) http_max_header_list_items : Option < usize > ,
135+
119136 /// Limit the depth of nested list fields in introspection queries
120137 /// to protect avoid generating huge responses. Returns a GraphQL
121138 /// error with `{ message: "Maximum introspection depth exceeded" }`
@@ -136,6 +153,8 @@ impl Default for Config {
136153 http_max_request_bytes : 2_000_000 ,
137154 http1_max_request_headers : None ,
138155 http1_max_request_buf_size : None ,
156+ http_max_request_headers : None ,
157+ http_max_header_list_items : None ,
139158 parser_max_tokens : 15_000 ,
140159
141160 // This is `apollo-parser`’s default, which protects against stack overflow
@@ -174,6 +193,12 @@ impl Plugin for LimitsPlugin {
174193 // Here we need to convert to and from the underlying http request types so that we can use existing middleware.
175194 . map_request ( Into :: into)
176195 . map_response ( Into :: into)
196+ . layer ( RequestHeaderCountLimitLayer :: new (
197+ self . config . http_max_request_headers ,
198+ ) )
199+ . layer ( RequestHeaderListItemsLimitLayer :: new (
200+ self . config . http_max_header_list_items ,
201+ ) )
177202 . layer ( RequestBodyLimitLayer :: new (
178203 self . config . http_max_request_bytes ,
179204 ) )
@@ -209,7 +234,10 @@ impl LimitsPlugin {
209234 }
210235
211236 match root_cause. downcast_ref :: < BodyLimitError > ( ) {
212- None => Err ( e) ,
237+ None => match root_cause. downcast_ref :: < HeaderLimitError > ( ) {
238+ None => Err ( e) ,
239+ Some ( header_error) => Ok ( header_error. into_response ( ctx) ) ,
240+ } ,
213241 Some ( _) => Ok ( BodyLimitError :: PayloadTooLarge . into_response ( ctx) ) ,
214242 }
215243 }
@@ -236,6 +264,37 @@ impl BodyLimitError {
236264 }
237265}
238266
267+ impl HeaderLimitError {
268+ fn into_response ( & self , ctx : Context ) -> router:: Response {
269+ match self {
270+ HeaderLimitError :: TooManyHeaders => router:: Response :: error_builder ( )
271+ . error (
272+ graphql:: Error :: builder ( )
273+ . message ( "Request header fields too many" )
274+ . extension_code ( "INVALID_GRAPHQL_REQUEST" )
275+ . extension ( "details" , "Request header fields too many" )
276+ . build ( ) ,
277+ )
278+ . status_code ( StatusCode :: REQUEST_HEADER_FIELDS_TOO_LARGE )
279+ . context ( ctx)
280+ . build ( )
281+ . unwrap ( ) ,
282+ HeaderLimitError :: TooManyHeaderListItems => router:: Response :: error_builder ( )
283+ . error (
284+ graphql:: Error :: builder ( )
285+ . message ( "Request header list too many items" )
286+ . extension_code ( "INVALID_GRAPHQL_REQUEST" )
287+ . extension ( "details" , "Request header list too many items" )
288+ . build ( ) ,
289+ )
290+ . status_code ( StatusCode :: REQUEST_HEADER_FIELDS_TOO_LARGE )
291+ . context ( ctx)
292+ . build ( )
293+ . unwrap ( ) ,
294+ }
295+ }
296+ }
297+
239298register_plugin ! ( "apollo" , "limits" , LimitsPlugin ) ;
240299
241300#[ cfg( test) ]
@@ -430,6 +489,121 @@ mod test {
430489 ) ;
431490 }
432491
492+ #[ tokio:: test]
493+ async fn test_header_count_limit_exceeded ( ) {
494+ let plugin = header_count_plugin ( ) . await ;
495+ let resp = plugin
496+ . router_service ( |_| async { panic ! ( "should have rejected request" ) } )
497+ . call (
498+ router:: Request :: fake_builder ( )
499+ . header ( "header1" , "value1" )
500+ . header ( "header2" , "value2" )
501+ . header ( "header3" , "value3" )
502+ . header ( "header4" , "value4" )
503+ . header ( "header5" , "value5" )
504+ . header ( "header6" , "value6" ) // This should exceed the limit of 5
505+ . body ( router:: body:: empty ( ) )
506+ . build ( )
507+ . unwrap ( ) ,
508+ )
509+ . await ;
510+ assert ! ( resp. is_ok( ) ) ;
511+ let resp = resp. unwrap ( ) ;
512+ assert_eq ! ( resp. response. status( ) , StatusCode :: REQUEST_HEADER_FIELDS_TOO_LARGE ) ;
513+ let body_str = String :: from_utf8 (
514+ router:: body:: into_bytes ( resp. response . into_body ( ) )
515+ . await
516+ . unwrap ( )
517+ . to_vec ( )
518+ ) . unwrap ( ) ;
519+ assert ! ( body_str. contains( "Request header fields too many" ) ) ;
520+ }
521+
522+ #[ tokio:: test]
523+ async fn test_header_count_limit_ok ( ) {
524+ let plugin = header_count_plugin ( ) . await ;
525+ let resp = plugin
526+ . router_service ( |_| async { Ok ( router:: Response :: fake_builder ( ) . build ( ) . unwrap ( ) ) } )
527+ . call (
528+ router:: Request :: fake_builder ( )
529+ . header ( "header1" , "value1" )
530+ . header ( "header2" , "value2" )
531+ . header ( "header3" , "value3" )
532+ . body ( router:: body:: empty ( ) )
533+ . build ( )
534+ . unwrap ( ) ,
535+ )
536+ . await ;
537+ assert ! ( resp. is_ok( ) ) ;
538+ let resp = resp. unwrap ( ) ;
539+ assert_eq ! ( resp. response. status( ) , StatusCode :: OK ) ;
540+ }
541+
542+ #[ tokio:: test]
543+ async fn test_header_list_items_limit_exceeded ( ) {
544+ let plugin = header_list_items_plugin ( ) . await ;
545+ let mut request = router:: Request :: fake_builder ( )
546+ . body ( router:: body:: empty ( ) ) ;
547+
548+ // Create a request with a header that has 4 values (exceeds limit of 3)
549+ for i in 1 ..=4 {
550+ request = request. header ( "test-header" , format ! ( "value{}" , i) ) ;
551+ }
552+
553+ let resp = plugin
554+ . router_service ( |_| async { panic ! ( "should have rejected request" ) } )
555+ . call ( request. build ( ) . unwrap ( ) )
556+ . await ;
557+ assert ! ( resp. is_ok( ) ) ;
558+ let resp = resp. unwrap ( ) ;
559+ assert_eq ! ( resp. response. status( ) , StatusCode :: REQUEST_HEADER_FIELDS_TOO_LARGE ) ;
560+ let body_str = String :: from_utf8 (
561+ router:: body:: into_bytes ( resp. response . into_body ( ) )
562+ . await
563+ . unwrap ( )
564+ . to_vec ( )
565+ ) . unwrap ( ) ;
566+ assert ! ( body_str. contains( "Request header list too many items" ) ) ;
567+ }
568+
569+ #[ tokio:: test]
570+ async fn test_header_list_items_limit_ok ( ) {
571+ let plugin = header_list_items_plugin ( ) . await ;
572+ let mut request = router:: Request :: fake_builder ( )
573+ . body ( router:: body:: empty ( ) ) ;
574+
575+ // Create a request with a header that has 2 values (within limit of 3)
576+ for i in 1 ..=2 {
577+ request = request. header ( "test-header" , format ! ( "value{}" , i) ) ;
578+ }
579+
580+ let resp = plugin
581+ . router_service ( |_| async { Ok ( router:: Response :: fake_builder ( ) . build ( ) . unwrap ( ) ) } )
582+ . call ( request. build ( ) . unwrap ( ) )
583+ . await ;
584+ assert ! ( resp. is_ok( ) ) ;
585+ let resp = resp. unwrap ( ) ;
586+ assert_eq ! ( resp. response. status( ) , StatusCode :: OK ) ;
587+ }
588+
589+ async fn header_count_plugin ( ) -> PluginTestHarness < LimitsPlugin > {
590+ let plugin: PluginTestHarness < LimitsPlugin > = PluginTestHarness :: builder ( )
591+ . config ( include_str ! ( "fixtures/header_count_limit.router.yaml" ) )
592+ . build ( )
593+ . await
594+ . expect ( "test harness" ) ;
595+ plugin
596+ }
597+
598+ async fn header_list_items_plugin ( ) -> PluginTestHarness < LimitsPlugin > {
599+ let plugin: PluginTestHarness < LimitsPlugin > = PluginTestHarness :: builder ( )
600+ . config ( include_str ! ( "fixtures/header_list_items_limit.router.yaml" ) )
601+ . build ( )
602+ . await
603+ . expect ( "test harness" ) ;
604+ plugin
605+ }
606+
433607 async fn plugin ( ) -> PluginTestHarness < LimitsPlugin > {
434608 let plugin: PluginTestHarness < LimitsPlugin > = PluginTestHarness :: builder ( )
435609 . config ( include_str ! ( "fixtures/content_length_limit.router.yaml" ) )
0 commit comments