@@ -11,6 +11,7 @@ use opentelemetry_sdk::metrics::SdkMeterProvider;
1111use rand:: Rng ;
1212use tokio_listener:: { Listener , SystemOptions , UserOptions } ;
1313use tower:: { Service , ServiceBuilder } ;
14+ use tower_http:: limit:: RequestBodyLimitLayer ;
1415use tracing:: info;
1516
1617use crate :: ohttp_relay:: SentinelTag ;
@@ -360,7 +361,8 @@ fn build_app(services: Services) -> Router {
360361 . layer (
361362 ServiceBuilder :: new ( )
362363 . layer ( axum:: middleware:: from_fn_with_state ( metrics. clone ( ) , track_metrics) )
363- . layer ( axum:: middleware:: from_fn_with_state ( metrics, track_connections) ) ,
364+ . layer ( axum:: middleware:: from_fn_with_state ( metrics, track_connections) )
365+ . layer ( RequestBodyLimitLayer :: new ( crate :: directory:: V1_MAX_BUFFER_SIZE ) ) ,
364366 )
365367 . with_state ( services) ;
366368
@@ -565,4 +567,97 @@ mod tests {
565567 assert ! ( metric_names. contains( & TOTAL_CONNECTIONS ) , "missing total_connections" ) ;
566568 assert ! ( metric_names. contains( & ACTIVE_CONNECTIONS ) , "missing active_connections" ) ;
567569 }
570+
571+ /// Exercises the full middleware stack (track_metrics, track_connections, RequestBodyLimit)
572+ /// with a request that has a body, ensuring the layer ordering compiles and runs correctly.
573+ #[ tokio:: test]
574+ async fn middleware_stack_accepts_request_with_body ( ) {
575+ use axum:: body:: Body ;
576+ use axum:: http:: header:: CONTENT_TYPE ;
577+ use axum:: http:: Request ;
578+ use tower:: ServiceExt ;
579+
580+ let tempdir = tempdir ( ) . unwrap ( ) ;
581+ let config = Config :: new (
582+ "[::]:0" . parse ( ) . expect ( "valid listener address" ) ,
583+ tempdir. path ( ) . to_path_buf ( ) ,
584+ Duration :: from_secs ( 2 ) ,
585+ None ,
586+ ) ;
587+
588+ let sentinel_tag = generate_sentinel_tag ( ) ;
589+ let services = Services {
590+ directory : init_directory ( & config, sentinel_tag) . await . unwrap ( ) ,
591+ relay : ohttp_relay:: Service :: new ( sentinel_tag) . await ,
592+ metrics : MetricsService :: new ( None ) ,
593+ #[ cfg( feature = "access-control" ) ]
594+ geoip : None ,
595+ } ;
596+
597+ let app = build_app ( services) ;
598+
599+ let body = b"small payload under 65k limit" ;
600+ let request = Request :: builder ( )
601+ . method ( "POST" )
602+ . uri ( "/some-path" )
603+ . header ( CONTENT_TYPE , ohttp_relay:: EXPECTED_MEDIA_TYPE . clone ( ) )
604+ . body ( Body :: from ( body. as_slice ( ) ) )
605+ . unwrap ( ) ;
606+
607+ let response = ServiceExt :: < Request < Body > > :: oneshot ( app, request) . await . unwrap ( ) ;
608+
609+ assert ! (
610+ !response. status( ) . is_server_error( ) ,
611+ "middleware stack should accept request with body (no 5xx)"
612+ ) ;
613+ assert_ne ! (
614+ response. status( ) ,
615+ axum:: http:: StatusCode :: PAYLOAD_TOO_LARGE ,
616+ "small body should not be rejected by body limit"
617+ ) ;
618+ }
619+
620+ /// Ensures RequestBodyLimitLayer (65_536 bytes) rejects oversized bodies (413 or 415).
621+ #[ tokio:: test]
622+ async fn request_body_limit_rejects_oversized_body ( ) {
623+ use axum:: body:: Body ;
624+ use axum:: http:: header:: CONTENT_TYPE ;
625+ use axum:: http:: Request ;
626+ use tower:: ServiceExt ;
627+
628+ let tempdir = tempdir ( ) . unwrap ( ) ;
629+ let config = Config :: new (
630+ "[::]:0" . parse ( ) . expect ( "valid listener address" ) ,
631+ tempdir. path ( ) . to_path_buf ( ) ,
632+ Duration :: from_secs ( 2 ) ,
633+ None ,
634+ ) ;
635+
636+ let sentinel_tag = generate_sentinel_tag ( ) ;
637+ let services = Services {
638+ directory : init_directory ( & config, sentinel_tag) . await . unwrap ( ) ,
639+ relay : ohttp_relay:: Service :: new ( sentinel_tag) . await ,
640+ metrics : MetricsService :: new ( None ) ,
641+ #[ cfg( feature = "access-control" ) ]
642+ geoip : None ,
643+ } ;
644+
645+ let app = build_app ( services) ;
646+
647+ let oversized: Vec < u8 > = ( 0 ..65_537 ) . map ( |_| 0u8 ) . collect ( ) ;
648+ let request = Request :: builder ( )
649+ . method ( "POST" )
650+ . uri ( "/" )
651+ . header ( CONTENT_TYPE , ohttp_relay:: EXPECTED_MEDIA_TYPE . clone ( ) )
652+ . body ( Body :: from ( oversized) )
653+ . unwrap ( ) ;
654+
655+ let response = ServiceExt :: < Request < Body > > :: oneshot ( app, request) . await . unwrap ( ) ;
656+
657+ assert ! (
658+ response. status( ) . is_client_error( ) ,
659+ "body over 65_536 bytes should be rejected with a 4xx status, got {}" ,
660+ response. status( )
661+ ) ;
662+ }
568663}
0 commit comments