From 3d2c74eb5e61b395d6c63b7db2b037155928a0b2 Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Tue, 14 Jul 2026 22:23:01 +0200 Subject: [PATCH 1/2] revert(http): restore pool.Exec worker delivery, drop the Connect-RPC proxy Signed-off-by: Valery Piashchynski --- config/config.go | 48 +- go.mod | 55 +- go.sum | 112 +- handler/convert.go | 38 +- handler/errors.go | 10 +- handler/errors_windows.go | 11 +- handler/handle_error_test.go | 52 - handler/handler.go | 394 ++-- handler/handler_test.go | 354 ++++ handler/parse.go | 69 +- handler/parse_test.go | 19 - handler/pool.go | 123 ++ handler/request.go | 317 +++- handler/response.go | 115 ++ handler/status_error.go | 41 - handler/status_error_test.go | 72 - handler/uploads.go | 13 +- plugin.go | 71 +- proxy/doc.go | 7 - proxy/queue.go | 172 -- proxy/queue_test.go | 228 --- proxy/server.go | 166 -- proxy/server_test.go | 131 -- servers/http11/http.go | 1 + servers/interface.go | 2 +- tests/configs/.rr-connectrpc-worker.yaml | 24 - tests/connectrpc_worker_test.go | 158 -- tests/go.mod | 87 +- tests/go.sum | 158 +- tests/handler_test.go | 2185 +++++++++++++++++----- tests/helpers/rpc.go | 50 - tests/helpers/worker.go | 177 -- tests/http_plugin2_test.go | 9 +- tests/http_plugin3_test.go | 103 +- tests/http_plugin4_test.go | 2 +- tests/http_plugin_test.go | 142 +- tests/php_test_files/big-resp | 1 + tests/php_test_files/worker-poc.log | 12 - tests/test_plugins/plugin1.go | 2 +- tests/test_plugins/plugin_middleware.go | 2 +- tests/uploads_test.go | 626 +++++-- tests/worker_poc_test.go | 100 - 42 files changed, 3673 insertions(+), 2786 deletions(-) delete mode 100644 handler/handle_error_test.go create mode 100644 handler/handler_test.go create mode 100644 handler/pool.go create mode 100644 handler/response.go delete mode 100644 handler/status_error.go delete mode 100644 handler/status_error_test.go delete mode 100644 proxy/doc.go delete mode 100644 proxy/queue.go delete mode 100644 proxy/queue_test.go delete mode 100644 proxy/server.go delete mode 100644 proxy/server_test.go delete mode 100644 tests/configs/.rr-connectrpc-worker.yaml delete mode 100644 tests/connectrpc_worker_test.go delete mode 100644 tests/helpers/rpc.go delete mode 100644 tests/helpers/worker.go create mode 100644 tests/php_test_files/big-resp delete mode 100644 tests/php_test_files/worker-poc.log delete mode 100644 tests/worker_poc_test.go diff --git a/config/config.go b/config/config.go index c247679..adc1c0c 100644 --- a/config/config.go +++ b/config/config.go @@ -2,7 +2,6 @@ package config import ( "strings" - "time" "github.com/roadrunner-server/http/v6/servers/fcgi" "github.com/roadrunner-server/http/v6/servers/http3" @@ -14,16 +13,16 @@ import ( // Config configures RoadRunner HTTP server. type Config struct { + // RawBody if turned on, RR will not parse the incoming HTTP body and will send it as is + RawBody bool `mapstructure:"raw_body"` // Host and port to handle as http server. Address string `mapstructure:"address"` // AccessLogs turn on/off, logged at Info log level, default: false AccessLogs bool `mapstructure:"access_logs"` // List of the middleware names (order will be preserved) Middleware []string `mapstructure:"middleware"` - // Pool configures worker pool (lifecycle only; requests are delivered via Proxy). + // Pool configures worker pool. Pool *pool.Config `mapstructure:"pool"` - // Proxy configures the worker-facing ConnectRPC server. - Proxy *Proxy `mapstructure:"proxy"` // InternalErrorCode used to override default 500 (InternalServerError) http code InternalErrorCode uint64 `mapstructure:"internal_error_code"` // MaxRequestSize specified max size for payload body in megabytes. 0 = 1GB. @@ -43,33 +42,6 @@ type Config struct { GID int } -// Proxy configures the ConnectRPC server that PHP workers connect into. -type Proxy struct { - // Address is the TCP address the proxy server listens on, e.g. ":7070". - Address string `mapstructure:"address"` - // RequestTimeout caps how long a single request can sit waiting for a - // worker to produce a response. Defaults to 60s. - RequestTimeout time.Duration `mapstructure:"request_timeout"` - // InboxSize bounds the in-process request queue. Submits beyond this - // return 503 to the client. Defaults to 1024. - InboxSize int `mapstructure:"inbox_size"` - // DebugMode flips the handler into debug mode (verbose error bodies on 5xx). - DebugMode bool `mapstructure:"debug"` -} - -func (p *Proxy) InitDefaults() { - if p.Address == "" { - // Bind to loopback by default - p.Address = "127.0.0.1:7070" - } - if p.RequestTimeout == 0 { - p.RequestTimeout = time.Minute - } - if p.InboxSize == 0 { - p.InboxSize = 1024 - } -} - // EnableHTTP is true when http server must run. func (c *Config) EnableHTTP() bool { return c.Address != "" @@ -106,11 +78,6 @@ func (c *Config) InitDefaults() error { } c.Pool.InitDefaults() - if c.Proxy == nil { - c.Proxy = &Proxy{} - } - c.Proxy.InitDefaults() - if c.InternalErrorCode == 0 { c.InternalErrorCode = 500 } @@ -157,15 +124,6 @@ func (c *Config) Valid() error { return errors.E(op, "malformed pool config") } - if c.Proxy != nil { - if c.Proxy.RequestTimeout < 0 { - return errors.E(op, errors.Str("proxy.request_timeout must be >= 0")) - } - if c.Proxy.InboxSize < 0 { - return errors.E(op, errors.Str("proxy.inbox_size must be >= 0")) - } - } - if !c.EnableHTTP() && !c.EnableTLS() && !c.EnableFCGI() { return errors.E(op, errors.Str("unable to run http service, no method has been specified (http, https, http/2 or FastCGI)")) } diff --git a/go.mod b/go.mod index f487d33..ee40304 100644 --- a/go.mod +++ b/go.mod @@ -2,31 +2,28 @@ module github.com/roadrunner-server/http/v6 go 1.26 -toolchain go1.26.4 - require ( - connectrpc.com/connect v1.20.0 - github.com/caddyserver/certmagic v0.25.4 + github.com/caddyserver/certmagic v0.25.3 github.com/google/go-cmp v0.7.0 - github.com/google/uuid v1.6.0 github.com/mholt/acmez v1.2.0 github.com/prometheus/client_golang v1.23.2 - github.com/quic-go/quic-go v0.60.0 - github.com/roadrunner-server/api-go/v6 v6.0.0-beta.12 + github.com/quic-go/quic-go v0.59.0 + github.com/roadrunner-server/api-go/v6 v6.0.0-beta.12.0.20260714200341-93604e5012d4 github.com/roadrunner-server/api-plugins/v6 v6.0.0-beta.2 github.com/roadrunner-server/context v1.3.0 github.com/roadrunner-server/endure/v2 v2.6.2 github.com/roadrunner-server/errors v1.5.0 + github.com/roadrunner-server/goridge/v4 v4.0.0-beta.2.0.20260714195909-75e9ece43063 github.com/roadrunner-server/pool/v2 v2.0.0-beta.1 github.com/roadrunner-server/tcplisten v1.5.2 github.com/stretchr/testify v1.11.1 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 - go.opentelemetry.io/contrib/propagators/jaeger v1.44.0 - go.opentelemetry.io/otel v1.44.0 - go.opentelemetry.io/otel/trace v1.44.0 - golang.org/x/net v0.56.0 - golang.org/x/sys v0.46.0 - google.golang.org/genproto v0.0.0-20260630182238-925bb5da69e7 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 + go.opentelemetry.io/contrib/propagators/jaeger v1.43.0 + go.opentelemetry.io/otel v1.43.0 + go.opentelemetry.io/otel/trace v1.43.0 + golang.org/x/net v0.54.0 + golang.org/x/sys v0.44.0 + google.golang.org/genproto v0.0.0-20260504160031-60b97b32f348 google.golang.org/protobuf v1.36.11 ) @@ -35,39 +32,39 @@ require ( github.com/caddyserver/zerossl v0.1.5 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/felixge/httpsnoop v1.1.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect - github.com/klauspost/cpuid/v2 v2.4.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/libdns/libdns v1.1.1 // indirect github.com/mholt/acmez/v3 v3.1.6 // indirect github.com/miekg/dns v1.1.72 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.69.0 // indirect - github.com/prometheus/procfs v0.21.1 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.20.1 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/roadrunner-server/events v1.0.1 // indirect - github.com/roadrunner-server/goridge/v4 v4.0.0-beta.2 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect - github.com/tklauser/go-sysconf v0.4.0 // indirect - github.com/tklauser/numcpus v0.12.0 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/zeebo/blake3 v0.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect go.uber.org/mock v0.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.28.0 // indirect go.uber.org/zap/exp v0.3.0 // indirect - golang.org/x/crypto v0.53.0 // indirect - golang.org/x/mod v0.37.0 // indirect - golang.org/x/sync v0.21.0 // indirect - golang.org/x/text v0.38.0 // indirect - golang.org/x/tools v0.47.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 // indirect - google.golang.org/grpc v1.82.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + golang.org/x/crypto v0.51.0 // indirect + golang.org/x/mod v0.36.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/text v0.37.0 // indirect + golang.org/x/tools v0.45.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 62c8e83..3ba9861 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,17 @@ code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE= code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM= -connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ= -connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/caddyserver/certmagic v0.25.4 h1:8eIXh0HC3MsGnNo8One+BCxMGTbe5zb/oz+2KsxBFQg= -github.com/caddyserver/certmagic v0.25.4/go.mod h1:YVs43D5+H/Dckt4bTga1KSO/xYfFBfVZainGDywYPAA= +github.com/caddyserver/certmagic v0.25.3 h1:mGf5ba8F7xA4c5jfDZZbK2buY1VEkbnwpMDixaju94A= +github.com/caddyserver/certmagic v0.25.3/go.mod h1:YVs43D5+H/Dckt4bTga1KSO/xYfFBfVZainGDywYPAA= github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE= github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= -github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -24,14 +22,12 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= -github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -56,18 +52,16 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.69.0 h1:OA85nJQS/T/MaYh/Q2CcgDKSGWqNIgrBDvDH85CuiNk= -github.com/prometheus/common v0.69.0/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y= -github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= -github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= -github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0= -github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= -github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0= -github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk= -github.com/roadrunner-server/api-go/v6 v6.0.0-beta.12 h1:FcRcCvW9OfQvH45SFsI21VoHpOOov56OvOSnO4UKvXs= -github.com/roadrunner-server/api-go/v6 v6.0.0-beta.12/go.mod h1:prGWJ2GoF5YD5PIG7Tb6VKulU3bWoFwr9DCwgxheb80= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/roadrunner-server/api-go/v6 v6.0.0-beta.12.0.20260714200341-93604e5012d4 h1:G5RlEP+rKKdarSw/ZcpWlpyrCne1AbuSZB8p9TagU3I= +github.com/roadrunner-server/api-go/v6 v6.0.0-beta.12.0.20260714200341-93604e5012d4/go.mod h1:Y4rsabWjr4Y10Jg6H8J5NDitQqlnXmGhCdgR+zyLYkI= github.com/roadrunner-server/api-plugins/v6 v6.0.0-beta.2 h1:GqsZzWQ5jMXRF1O/b8IqFz9PLpS7Ui0K4OyACLql2MI= github.com/roadrunner-server/api-plugins/v6 v6.0.0-beta.2/go.mod h1:2v4yUK5Kvbvq8C3IkDoBkuamq9h+7i/JLjyf7k1j5JM= github.com/roadrunner-server/context v1.3.0 h1:iyTXVORhPU2/26z7kdzEaggwG5P8yhIKUDLiePjylFQ= @@ -78,8 +72,8 @@ github.com/roadrunner-server/errors v1.5.0 h1:unG7LKIZrSzkCCF3YLRLA5VyqE0KKomofX github.com/roadrunner-server/errors v1.5.0/go.mod h1:g9fo/T2C13cWRDR9PW1r0ZAOSQfNhWAZawyfkGiaHuI= github.com/roadrunner-server/events v1.0.1 h1:waCkKhxhzdK3VcI1xG22l+h+0J+Nfdpxjhyy01Un+kI= github.com/roadrunner-server/events v1.0.1/go.mod h1:WZRqoEVaFm209t52EuoT7ISUtvX6BrCi6bI/7pjkVC0= -github.com/roadrunner-server/goridge/v4 v4.0.0-beta.2 h1:MgH6oiSgcl+vphsQ6JpyedkXQ/DPf8zVpn0z7rdBp10= -github.com/roadrunner-server/goridge/v4 v4.0.0-beta.2/go.mod h1:Wv9CBO9VIU92e5iZIuehLHKakXgMkOzxoT4/oHDjIUA= +github.com/roadrunner-server/goridge/v4 v4.0.0-beta.2.0.20260714195909-75e9ece43063 h1:0mNGmXgYR2/hUhPQde+GJFzj/UQ6vqrLAHcwa5C5rqA= +github.com/roadrunner-server/goridge/v4 v4.0.0-beta.2.0.20260714195909-75e9ece43063/go.mod h1:1aHppV68y/VqRED/AsfNg59sft9aQOhqgr5Z5n49jbM= github.com/roadrunner-server/pool/v2 v2.0.0-beta.1 h1:jpYXFtdD6QGAdAGPgMxrNi3j1CegCRpb2y+A+3GnXFA= github.com/roadrunner-server/pool/v2 v2.0.0-beta.1/go.mod h1:Bo1wT7RtL3eyQHXBUohNhtj/yAmRt6Rq8smuBg5pWkY= github.com/roadrunner-server/tcplisten v1.5.2 h1:nn8yXYrhRDkfQ9AAu4V075uT4fZRmOnpxkawgE+bWPA= @@ -90,10 +84,10 @@ github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKl github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU= -github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI= -github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4= -github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= @@ -104,20 +98,20 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= -go.opentelemetry.io/contrib/propagators/jaeger v1.44.0 h1:OyzvsAMc/zHt0DRPcfstn0wgfq8ApDkeY0ABMcueweM= -go.opentelemetry.io/contrib/propagators/jaeger v1.44.0/go.mod h1:44kghcGX+BNxy9UTiWtd6VDt8Nd4EypGBkH2+v2Dqrc= -go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= -go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= -go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= -go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= -go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= -go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= -go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= -go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= -go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= -go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= +go.opentelemetry.io/contrib/propagators/jaeger v1.43.0 h1:peiLMz1+aqJE+3L4mOVtR9wlmv+yh/JVYXCBjqmzJJE= +go.opentelemetry.io/contrib/propagators/jaeger v1.43.0/go.mod h1:Agvif+4A8p/3UtZzJ0MCcDEuQwgtrzM71DueU41DCs8= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= @@ -132,30 +126,24 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= -golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= -golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= -golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= -gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= -gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto v0.0.0-20260630182238-925bb5da69e7 h1:lQG76ePMKmtujel4VIVMiFoHVWVNtJdawbCZJtWlVXU= -google.golang.org/genproto v0.0.0-20260630182238-925bb5da69e7/go.mod h1:LwlOWYBU335L+sR55UuR5fbbU8KmEX+3tUHf3SwMmhM= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 h1:eM/YSd5bBFagF51o1E745Ta7RwzpW0h+z+QDNZOgmQ8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU= -google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +google.golang.org/genproto v0.0.0-20260504160031-60b97b32f348 h1:JjVGDZYWkJWZcxveJGzfkXC5myDVWAd4dZdgbzrDUv8= +google.golang.org/genproto v0.0.0-20260504160031-60b97b32f348/go.mod h1:95PqD4xM+AdOcBGsmgfaofXsiA37uXDtDufVbntT3TU= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/handler/convert.go b/handler/convert.go index 100a1f2..9031e68 100644 --- a/handler/convert.go +++ b/handler/convert.go @@ -3,41 +3,41 @@ package handler import ( "net/http" - httpV2 "github.com/roadrunner-server/api-go/v6/http/v2" + httpV2proto "github.com/roadrunner-server/api-go/v6/http/v2" ) -func convert(headers http.Header) map[string]*httpV2.HttpHeaderValue { +func convert(headers http.Header) map[string]*httpV2proto.HttpHeaderValue { if len(headers) == 0 { return nil } - resp := make(map[string]*httpV2.HttpHeaderValue, len(headers)) + resp := make(map[string]*httpV2proto.HttpHeaderValue, len(headers)) + for k, v := range headers { - resp[k] = &httpV2.HttpHeaderValue{Values: v} - } - return resp -} + if resp[k] == nil { + resp[k] = &httpV2proto.HttpHeaderValue{} + } -func convertCookies(cookies map[string]string) map[string]*httpV2.HttpHeaderValue { - if len(cookies) == 0 { - return nil + resp[k].Values = append(resp[k].GetValues(), v...) } - resp := make(map[string]*httpV2.HttpHeaderValue, len(cookies)) - for k, v := range cookies { - resp[k] = &httpV2.HttpHeaderValue{Values: []string{v}} - } return resp } -func convertAttributes(attrs map[string][]string) map[string]*httpV2.HttpHeaderValue { - if len(attrs) == 0 { +func convertCookies(headers map[string]string) map[string]*httpV2proto.HttpHeaderValue { + if len(headers) == 0 { return nil } - resp := make(map[string]*httpV2.HttpHeaderValue, len(attrs)) - for k, v := range attrs { - resp[k] = &httpV2.HttpHeaderValue{Values: v} + resp := make(map[string]*httpV2proto.HttpHeaderValue, len(headers)) + + for k, v := range headers { + if resp[k] == nil { + resp[k] = &httpV2proto.HttpHeaderValue{} + } + + resp[k].Values = append(resp[k].GetValues(), v) } + return resp } diff --git a/handler/errors.go b/handler/errors.go index c6aced8..8d3855f 100644 --- a/handler/errors.go +++ b/handler/errors.go @@ -2,9 +2,9 @@ package handler -import "syscall" +import ( + "errors" +) -// errEPIPE is the OS broken-pipe error. Matches via errors.Is when the -// underlying write/read syscall returned EPIPE (wrapped in *os.SyscallError / -// *net.OpError chains by the stdlib). -var errEPIPE = syscall.EPIPE +// Broken pipe +var errEPIPE = errors.New("EPIPE(32) -> connection reset by peer") diff --git a/handler/errors_windows.go b/handler/errors_windows.go index 4d8c74b..3767bfa 100644 --- a/handler/errors_windows.go +++ b/handler/errors_windows.go @@ -2,8 +2,11 @@ package handler -import "syscall" +import ( + "errors" +) -// errEPIPE is the Windows analogue of EPIPE — a connection aborted by the -// software in the host computer (data-transmission timeout or protocol error). -var errEPIPE = syscall.WSAECONNABORTED +// Software caused connection abort. +// An established connection was aborted by the software in your host computer, +// possibly due to a data transmission time-out or protocol error. +var errEPIPE = errors.New("WSAECONNABORTED (10053) -> an established connection was aborted by peer") diff --git a/handler/handle_error_test.go b/handler/handle_error_test.go deleted file mode 100644 index 225720c..0000000 --- a/handler/handle_error_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package handler - -import ( - "errors" - "io" - "log/slog" - "net/http" - "net/http/httptest" - "strings" - "testing" -) - -func TestHandleError_WritesBodyAndHeaders(t *testing.T) { - cases := []struct { - name string - debugMode bool - wantBody string // substring match (http.Error appends a trailing newline) - }{ - {name: "non-debug uses StatusText", debugMode: false, wantBody: http.StatusText(http.StatusInternalServerError)}, - {name: "debug exposes error message", debugMode: true, wantBody: "boom"}, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - h := &Handler{ - log: slog.New(slog.NewTextHandler(io.Discard, nil)), - internalHTTPCode: http.StatusInternalServerError, - debugMode: tc.debugMode, - } - rec := httptest.NewRecorder() - - h.handleError(rec, errors.New("boom")) - - resp := rec.Result() - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusInternalServerError { - t.Errorf("status: got %d, want %d", resp.StatusCode, http.StatusInternalServerError) - } - if ct := resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text/plain") { - t.Errorf("Content-Type: got %q, want text/plain prefix", ct) - } - if nosniff := resp.Header.Get("X-Content-Type-Options"); nosniff != "nosniff" { - t.Errorf("X-Content-Type-Options: got %q, want nosniff", nosniff) - } - body, _ := io.ReadAll(resp.Body) - if !strings.Contains(string(body), tc.wantBody) { - t.Errorf("body: got %q, want substring %q", body, tc.wantBody) - } - }) - } -} diff --git a/handler/handler.go b/handler/handler.go index 429a889..5846eb8 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -1,315 +1,241 @@ package handler import ( - "encoding/json" + "context" stderr "errors" - "fmt" + "html/template" "log/slog" "net/http" - "strings" "sync" "time" - "github.com/google/uuid" - httpV2 "github.com/roadrunner-server/api-go/v6/http/v2" + "github.com/roadrunner-server/http/v6/api" + + httpV2proto "github.com/roadrunner-server/api-go/v6/http/v2" "github.com/roadrunner-server/errors" - "github.com/roadrunner-server/http/v6/attributes" + "github.com/roadrunner-server/goridge/v4/pkg/frame" "github.com/roadrunner-server/http/v6/config" - "github.com/roadrunner-server/http/v6/proxy" + "github.com/roadrunner-server/pool/v2/payload" ) const ( - Trailer = "Trailer" - HTTP2Push = "Http2-Push" + noWorkers string = "No-Workers" + trueStr string = "true" ) -type uploadsCfg struct { +var _ http.Handler = (*Handler)(nil) + +type uploads struct { dir string allow map[string]struct{} forbid map[string]struct{} } -// Handler serves HTTP requests by handing them off to PHP workers connected -// over ConnectRPC (via proxy.Queue). It does not own worker lifecycle. +// Handler serves http connections to underlying PHP application using PSR-7 protocol. Context will include request headers, +// parsed files and query, payload will include parsed form dataTree (if any). type Handler struct { - queue *proxy.Queue - uploads *uploadsCfg - log *slog.Logger + uploads *uploads + log *slog.Logger + pool api.Pool + internalCtx context.Context internalHTTPCode uint64 - requestTimeout time.Duration + sendRawBody bool debugMode bool + // permissions uid int gid int - // reqPool reuses *HttpHandlerRequest envelopes across requests. - reqPool sync.Pool + // internal + reqPool sync.Pool + protoRespPool sync.Pool + protoReqPool sync.Pool + pldPool sync.Pool + stopChPool sync.Pool } -var _ http.Handler = (*Handler)(nil) - -func NewHandler(cfg *config.Config, queue *proxy.Queue, log *slog.Logger) *Handler { +// NewHandler return 'handler' interface implementation +func NewHandler(cfg *config.Config, pool api.Pool, log *slog.Logger) (*Handler, error) { return &Handler{ - queue: queue, - log: log, - uploads: &uploadsCfg{ + uploads: &uploads{ dir: cfg.Uploads.Dir, allow: cfg.Uploads.Allowed, forbid: cfg.Uploads.Forbidden, }, + pool: pool, + debugMode: checkDebug(cfg), + log: log, internalHTTPCode: cfg.InternalErrorCode, - requestTimeout: cfg.Proxy.RequestTimeout, - debugMode: cfg.Proxy.DebugMode, - uid: cfg.UID, - gid: cfg.GID, - reqPool: sync.Pool{ - New: func() any { return &httpV2.HttpHandlerRequest{} }, - }, - } -} + sendRawBody: cfg.RawBody, + internalCtx: context.Background(), -func (h *Handler) getReq() *httpV2.HttpHandlerRequest { - return h.reqPool.Get().(*httpV2.HttpHandlerRequest) -} + // permissions + uid: cfg.UID, + gid: cfg.GID, -func (h *Handler) putReq(req *httpV2.HttpHandlerRequest) { - req.Reset() - h.reqPool.Put(req) + stopChPool: sync.Pool{ + New: func() any { + return make(chan struct{}, 1) + }, + }, + reqPool: sync.Pool{ + New: func() any { + return &Request{ + Attributes: make(map[string][]string), + Cookies: make(map[string]string), + body: nil, + } + }, + }, + protoRespPool: sync.Pool{ + New: func() any { + return &httpV2proto.HttpHandlerResponse{ + Headers: make(map[string]*httpV2proto.HttpHeaderValue), + Status: -1, + } + }, + }, + protoReqPool: sync.Pool{ + New: func() any { + return &httpV2proto.HttpHandlerRequest{} + }, + }, + pldPool: sync.Pool{ + New: func() any { + return &payload.Payload{ + Body: make([]byte, 0, 100), + Context: make([]byte, 0, 100), + Codec: frame.CodecProto, + } + }, + }, + }, nil } -// ServeHTTP builds a HttpHandlerRequest, submits it to the queue, and blocks -// on the per-request response channel. Multipart uploads are extracted to the -// configured tmpdir; everything else has its body passed through raw. +// ServeHTTP transform the original request to the PSR-7 passed then to the underlying application. Attempts to serve static files first if enabled. func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { const op = errors.Op("serve_http") start := time.Now() - id, err := uuid.NewV7() - if err != nil { - h.handleError(w, errors.E(op, err)) - h.log.Error("uuid", "elapsed", time.Since(start).Milliseconds(), "error", err) - return - } - - // Envelopes are returned to the pool ONLY on the happy path after we've - // received a response — at that point ConnectRPC has finished writing the - // request to the wire and no worker still references the pointer. On - // timeout / disconnect / submit error paths a worker may still hold the - // pointer (it could still be parked in the inbox, or be mid-marshal in - // FetchRequest's response), so we leak the envelope to GC instead of - // Reset()-corrupting it. - req := h.getReq() - req.Id = id.String() - req.Method = r.Method - req.Uri = URI(r) - req.Protocol = r.Proto - req.RemoteAddr = FetchIP(r.RemoteAddr, h.log) - req.Header = convert(r.Header) - req.Cookies = convertCookies(extractCookies(r)) - req.RawQuery = cleanRawQuery(r.URL.RawQuery) - req.Attributes = convertAttributes(attributes.All(r)) - - ups, err := populateBody(r, req, h.uid, h.gid) + req := h.getReq(r) + err := request(r, req, h.uid, h.gid, h.sendRawBody) if err != nil { - h.handleRequestErr(w, r, ups, err, start) - return - } - if ups != nil { - // Open mutates each FileUpload (Error / Size / TempFilename) — so we - // marshal req.Uploads AFTER, not before, to capture the final state. - ups.Open(h.log, h.uploads.dir, h.uploads.forbid, h.uploads.allow) - if req.Uploads, err = json.Marshal(ups); err != nil { - // Marshaling our own Uploads struct is a server-side bug, not - // client input — keep the 5xx semantics rather than letting it - // fall through handleRequestErr's 4xx default. - clearUploads(h.log, r, ups) - h.handleError(w, err) - h.log.Error("marshal uploads", + // if the pipe is broken, there is no sense to write the header + // in this case, we just report about error + if stderr.Is(err, errEPIPE) { + req.Close(h.log, r) + h.putReq(req) + h.log.Error( + "write response error", + "start", start, "elapsed", time.Since(start).Milliseconds(), "error", err, ) return } - } - respCh, err := h.queue.Submit(req) - if err != nil { - clearUploads(h.log, r, ups) - h.handleSubmitErr(w, err) - h.log.Error("queue submit", - "id", req.GetId(), + req.Close(h.log, r) + h.putReq(req) + // a request-forming error is a bad request, not a server fault: + // request() only touches client-supplied bytes. Default to 400 and let + // the size-limit branch bump it to 413. + status := http.StatusBadRequest + if _, ok := stderr.AsType[*http.MaxBytesError](err); ok { + status = http.StatusRequestEntityTooLarge + } + http.Error(w, errors.E(op, err).Error(), status) + h.log.Error( + "request forming error", + "start", start, "elapsed", time.Since(start).Milliseconds(), "error", err, ) return } - timeout := time.NewTimer(h.requestTimeout) - defer timeout.Stop() - - reqID := req.GetId() - - select { - case resp := <-respCh: - h.writeResponse(w, resp, start, reqID) + req.Open(h.log, h.uploads.dir, h.uploads.forbid, h.uploads.allow) + // get payload from the pool + pld := h.getPld() + // get proto request from the pool + reqproto := h.getProtoReq(req) + err = req.Payload(pld, h.sendRawBody, reqproto) + h.putProtoReq(reqproto) + if err != nil { + req.Close(h.log, r) h.putReq(req) - case <-r.Context().Done(): - h.queue.Cancel(reqID) - h.log.Debug("client disconnected", - "id", reqID, - "elapsed", time.Since(start).Milliseconds(), - ) - case <-timeout.C: - h.queue.Cancel(reqID) - http.Error(w, "gateway timeout", http.StatusGatewayTimeout) - h.log.Warn("request timeout", - "id", reqID, - "elapsed", time.Since(start).Milliseconds(), - ) - } - - clearUploads(h.log, r, ups) -} - -func (h *Handler) writeResponse(w http.ResponseWriter, resp *httpV2.HttpHandlerResponse, start time.Time, id string) { - status := int(resp.GetStatus()) - if status < 100 || status >= 600 { - // Validate status BEFORE writing any worker-supplied headers — otherwise - // the 500 we serve here would carry Set-Cookie / Location / etc. from - // the bogus response. - http.Error(w, fmt.Sprintf("unknown status code from worker: %d", status), http.StatusInternalServerError) - h.log.Error("invalid worker status", - "id", id, - "status", status, + h.putPld(pld) + h.handleError(w, err) + h.log.Error( + "payload forming error", + "start", start, "elapsed", time.Since(start).Milliseconds(), + "error", err, ) return } - headers := resp.GetHeaders() - - if push := headers[HTTP2Push]; push != nil { - if pusher, ok := w.(http.Pusher); ok { - for _, target := range push.GetValues() { - if err := pusher.Push(target, nil); err != nil { - h.log.Warn("http/2 push", "id", id, "target", target, "error", err) - } - } - } - } - - if headers[Trailer] != nil { - handleProtoTrailers(headers) - } - - for k, v := range headers { - // Http2-Push is plugin-internal control metadata — already consumed - // for the push above. Don't echo it back to the client. - if k == HTTP2Push { - continue - } - for _, vv := range v.GetValues() { - w.Header().Add(k, vv) - } - } - - w.WriteHeader(status) - - body := resp.GetBody() - if len(body) == 0 { + stopCh := h.getCh() + wResp, err := h.pool.Exec(h.internalCtx, pld, stopCh) + if err != nil { + req.Close(h.log, r) + h.putReq(req) + h.putPld(pld) + h.putCh(stopCh) + h.handleError(w, err) + h.log.Error("execute", "start", start, "elapsed", time.Since(start).Milliseconds(), "error", err) return } - if _, err := w.Write(body); err != nil { - if stderr.Is(err, errEPIPE) { - h.log.Debug("response write: broken pipe", - "id", id, + // return payload to the pool + h.putPld(pld) + + for recv := range wResp { + if recv.Error() != nil { + req.Close(h.log, r) + h.putReq(req) + h.putCh(stopCh) + w.WriteHeader(int(h.internalHTTPCode)) //nolint:gosec + h.log.Error("read stream", + "start", start, "elapsed", time.Since(start).Milliseconds(), - ) + "error", recv.Error()) return } - h.log.Error("response write", - "id", id, - "elapsed", time.Since(start).Milliseconds(), - "error", err, - ) - } - - if fl, ok := w.(http.Flusher); ok { - fl.Flush() - } -} -func handleProtoTrailers(h map[string]*httpV2.HttpHeaderValue) { - for _, tr := range h[Trailer].GetValues() { - for n := range strings.SplitSeq(tr, ",") { - n = strings.Trim(n, "\t ") - if v, ok := h[n]; ok { - h["Trailer:"+n] = v - delete(h, n) + err = h.Write(recv.Payload(), w) + if err != nil { + // send a stop signal to the worker pool + select { + case stopCh <- struct{}{}: + default: } - } - } - delete(h, Trailer) -} - -// handleRequestErr writes the response for an error produced while parsing the -// client's request. Such errors are 4xx by construction — populateBody only -// touches client-supplied bytes (multipart form, body, query). The default is -// http.StatusBadRequest; call sites that know a more specific code (e.g. 413 -// for size limits) wrap the error with withStatus, which wins here via -// errors.As. Server-internal failures must not flow through this path — see -// handleError instead. -func (h *Handler) handleRequestErr(w http.ResponseWriter, r *http.Request, ups *Uploads, err error, start time.Time) { - clearUploads(h.log, r, ups) - - if stderr.Is(err, errEPIPE) { - h.log.Error("request decode: broken pipe", - "elapsed", time.Since(start).Milliseconds(), - "error", err, - ) - return - } - status := http.StatusBadRequest - if sErr, ok := stderr.AsType[*statusError](err); ok { - status = sErr.Status() + // we should not exit from the loop here, since after sending close signal, it should be closed from the SDK side + h.log.Error("write response (chunk) error", + "start", start, + "elapsed", time.Since(start).Milliseconds(), + "error", err) + } } - http.Error(w, err.Error(), status) - h.log.Error("request decode", - "elapsed", time.Since(start).Milliseconds(), - "error", err, - ) -} - -func (h *Handler) handleSubmitErr(w http.ResponseWriter, err error) { - if stderr.Is(err, proxy.ErrInboxFull) { - http.Error(w, "service unavailable", http.StatusServiceUnavailable) - return - } - h.handleError(w, err) + req.Close(h.log, r) + h.putReq(req) + h.putCh(stopCh) } +// handleError will handle internal RR errors and return 500 func (h *Handler) handleError(w http.ResponseWriter, err error) { - // internalHTTPCode is a config-provided HTTP status, defaulted to 500 and - // always within [100, 599] in practice. The cast is safe. - status := int(h.internalHTTPCode) //nolint:gosec // G115: bounded HTTP status code - msg := http.StatusText(status) - if h.debugMode { - msg = err.Error() + // if there are no free workers -> write a special header + if errors.Is(errors.NoFreeWorkers, err) { + // set header for the prometheus + w.Header().Set(noWorkers, trueStr) } - // http.Error sets Content-Type and X-Content-Type-Options: nosniff and - // writes msg as the body — keeps the response well-formed even when the - // configured internalHTTPCode is a 5xx and we're not in debug mode. - http.Error(w, msg, status) -} -func clearUploads(log *slog.Logger, r *http.Request, ups *Uploads) { - if ups != nil { - ups.Clear(log) - } - if r.MultipartForm != nil { - _ = r.MultipartForm.RemoveAll() + // write an internal server error + w.WriteHeader(int(h.internalHTTPCode)) //nolint:gosec + + // in debug mode, write all output into the browser/curl/any_tool + if h.debugMode { + template.HTMLEscape(w, []byte(err.Error())) } } diff --git a/handler/handler_test.go b/handler/handler_test.go new file mode 100644 index 0000000..5c5e4aa --- /dev/null +++ b/handler/handler_test.go @@ -0,0 +1,354 @@ +package handler + +import ( + "context" + "crypto/tls" + "fmt" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + "log/slog" + + "github.com/roadrunner-server/errors" + "github.com/roadrunner-server/http/v6/api" + "github.com/roadrunner-server/http/v6/config" + "github.com/roadrunner-server/pool/v2/payload" + "github.com/roadrunner-server/pool/v2/pool" + staticPool "github.com/roadrunner-server/pool/v2/pool/static_pool" + "github.com/roadrunner-server/pool/v2/worker" +) + +// mockPool satisfies api.Pool. Only Exec is exercised by the tests below. +type mockPool struct{ execErr error } + +func (m *mockPool) Workers() []*worker.Process { return nil } +func (m *mockPool) RemoveWorker(_ context.Context) error { return nil } +func (m *mockPool) AddWorker() error { return nil } +func (m *mockPool) Exec(_ context.Context, _ *payload.Payload, _ chan struct{}) (chan *staticPool.PExec, error) { + return nil, m.execErr +} +func (m *mockPool) Reset(_ context.Context) error { return nil } +func (m *mockPool) Destroy(_ context.Context) {} + +func newTestHandler(t *testing.T, cfg *config.Config, p api.Pool) *Handler { + t.Helper() + h, err := NewHandler(cfg, p, slog.New(slog.DiscardHandler)) + if err != nil { + t.Fatal(err) + } + return h +} + +func defaultCfg() *config.Config { + return &config.Config{ + MaxRequestSize: 1024, + InternalErrorCode: 500, + Uploads: &config.Uploads{ + Dir: os.TempDir(), + Forbidden: map[string]struct{}{}, + Allowed: map[string]struct{}{}, + }, + } +} + +// ── Group A: ServeHTTP errors before pool.Exec (nil pool is safe) ──────────── + +func TestServeHTTP_InvalidMultipart_Returns400(t *testing.T) { + h := newTestHandler(t, defaultCfg(), nil) + + // Boundary declared in header but body has no valid multipart parts → EOF. + req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/", strings.NewReader("")) + req.Header.Set("Content-Type", "multipart/form-data; boundary=1111") + + rr := httptest.NewRecorder() + h.ServeHTTP(rr, req) + + if rr.Code != http.StatusBadRequest { + t.Errorf("expected 400, got %d", rr.Code) + } +} + +func TestServeHTTP_StreamBody_MaxBytesExceeded_Returns413(t *testing.T) { + h := newTestHandler(t, defaultCfg(), nil) + + req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/", strings.NewReader("this body is too long")) + req.Header.Set("Content-Type", "application/json") + + rr := httptest.NewRecorder() + // Wrap body so that reading more than 5 bytes returns *http.MaxBytesError. + req.Body = http.MaxBytesReader(rr, req.Body, 5) + + h.ServeHTTP(rr, req) + + if rr.Code != http.StatusRequestEntityTooLarge { + t.Errorf("expected 413, got %d", rr.Code) + } +} + +func TestServeHTTP_TruncatedMultipart_Returns400(t *testing.T) { + h := newTestHandler(t, defaultCfg(), nil) + + // Multipart body with an open part but no closing boundary → ErrUnexpectedEOF. + body := "--1111\r\nContent-Disposition: form-data; name=\"f\"\r\n\r\nval" + req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/", strings.NewReader(body)) + req.Header.Set("Content-Type", "multipart/form-data; boundary=1111") + + rr := httptest.NewRecorder() + h.ServeHTTP(rr, req) + + if rr.Code != http.StatusBadRequest { + t.Errorf("expected 400, got %d", rr.Code) + } +} + +// ── Group B: Direct calls to handleError, URI, FetchIP ─────────────────────── + +func TestHandleError_NoFreeWorkers_SetsNoWorkersHeader(t *testing.T) { + h := newTestHandler(t, defaultCfg(), nil) + + rr := httptest.NewRecorder() + h.handleError(rr, errors.E(errors.NoFreeWorkers)) + + if got := rr.Header().Get(noWorkers); got != trueStr { + t.Errorf("expected No-Workers: true, got %q", got) + } + if rr.Code != 500 { + t.Errorf("expected status 500, got %d", rr.Code) + } +} + +func TestHandleError_CustomInternalCode(t *testing.T) { + cfg := defaultCfg() + cfg.InternalErrorCode = 503 + h := newTestHandler(t, cfg, nil) + + rr := httptest.NewRecorder() + h.handleError(rr, fmt.Errorf("boom")) + + if rr.Code != 503 { + t.Errorf("expected 503, got %d", rr.Code) + } +} + +func TestHandleError_DebugMode_WritesEscapedError(t *testing.T) { + cfg := defaultCfg() + cfg.Pool = &pool.Config{Debug: true} + h := newTestHandler(t, cfg, nil) + + rr := httptest.NewRecorder() + h.handleError(rr, fmt.Errorf("boom