@@ -17,6 +17,7 @@ import (
1717 httpV2 "github.com/roadrunner-server/api-go/v6/http/v2"
1818 "github.com/roadrunner-server/http/v6/config"
1919 "github.com/roadrunner-server/http/v6/handler"
20+ httpMw "github.com/roadrunner-server/http/v6/middleware"
2021 "github.com/roadrunner-server/http/v6/proxy"
2122 "github.com/stretchr/testify/assert"
2223 "github.com/stretchr/testify/require"
@@ -469,6 +470,70 @@ func TestHandler_Multipart_PATCH(t *testing.T) {
469470 assertStandardFormTree (t , res , "value" )
470471}
471472
473+ // TestHandler_NonMultipart_OversizeBody guards against the regression where a
474+ // non-multipart body exceeding MaxRequestSize returned 400 instead of 413,
475+ // because the original handleRequestErr's explicit MaxBytesError case had
476+ // been collapsed into the 400 default. classifyParseErr now promotes the
477+ // MaxBytesError on the io.ReadAll path back to 413.
478+ func TestHandler_NonMultipart_OversizeBody (t * testing.T ) {
479+ const maxBytes = 64
480+ cfg := defaultCfg ()
481+ q := proxy .NewQueue (cfg .Proxy .InboxSize )
482+ stop := helpers .StartFakeWorker (t .Context (), q , multipartEchoResponder )
483+ t .Cleanup (stop )
484+
485+ h := handler .NewHandler (cfg , q , testLog .SlogLogger ())
486+ // Wrap with the same MaxRequestSize middleware the plugin applies in
487+ // production (init.go) — without it ReadAll never sees MaxBytesError.
488+ hs := & http.Server {
489+ Addr : "127.0.0.1:8190" ,
490+ Handler : httpMw .MaxRequestSize (h , maxBytes ),
491+ ReadHeaderTimeout : time .Minute ,
492+ }
493+ go func () {
494+ if err := hs .ListenAndServe (); err != nil && ! errors .Is (err , http .ErrServerClosed ) {
495+ t .Errorf ("listen: %v" , err )
496+ }
497+ }()
498+ t .Cleanup (func () { _ = hs .Shutdown (context .Background ()) })
499+ time .Sleep (10 * time .Millisecond )
500+
501+ body := strings .Repeat ("x" , int (maxBytes )* 4 )
502+ req , err := http .NewRequestWithContext (t .Context (), http .MethodPost , "http://127.0.0.1:8190/" , strings .NewReader (body ))
503+ require .NoError (t , err )
504+ req .Header .Set ("Content-Type" , "application/json" )
505+
506+ r , err := http .DefaultClient .Do (req )
507+ require .NoError (t , err )
508+ defer func () { _ = r .Body .Close () }()
509+
510+ assert .Equal (t , http .StatusRequestEntityTooLarge , r .StatusCode )
511+ }
512+
513+ // TestHandler_Multipart_SemicolonInQuery covers issue #2353: a malformed
514+ // query string causes ParseMultipartForm (which internally parses the URL
515+ // query) to fail with "invalid semicolon separator in query". The response
516+ // must be 400 Bad Request, not the historical 500.
517+ func TestHandler_Multipart_SemicolonInQuery (t * testing.T ) {
518+ env := newHandlerEnv (t , "127.0.0.1:8189" , defaultCfg (), multipartEchoResponder )
519+ defer env .close (t )
520+
521+ var mb bytes.Buffer
522+ w := multipart .NewWriter (& mb )
523+ require .NoError (t , w .WriteField ("key" , "value" ))
524+ require .NoError (t , w .Close ())
525+
526+ req , err := http .NewRequestWithContext (t .Context (), http .MethodPost , "http://127.0.0.1:8189/?a=b;c" , & mb )
527+ require .NoError (t , err )
528+ req .Header .Set ("Content-Type" , w .FormDataContentType ())
529+
530+ r , err := http .DefaultClient .Do (req )
531+ require .NoError (t , err )
532+ defer func () { _ = r .Body .Close () }()
533+
534+ assert .Equal (t , http .StatusBadRequest , r .StatusCode )
535+ }
536+
472537func doMultipartFormPost (t * testing.T , method , urlStr string ) map [string ]any {
473538 t .Helper ()
474539 var mb bytes.Buffer
0 commit comments