Skip to content

Commit 3e6f201

Browse files
gaboragerenovate[bot]claude
authored
feat(server): configurable body limit + group-404 guard hardening for echo v5.3.0 (#711)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2ebd6ee commit 3e6f201

19 files changed

Lines changed: 404 additions & 19 deletions

config.example.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ server:
2929
ready: /ready # Custom readiness endpoint path
3030
gzip:
3131
minlength: 1024 # Min response size (bytes) before gzip applies; smaller responses skip compression
32+
bodylimit: 10485760 # Max request body size in bytes (default 10 MB); raise for large uploads. 0 = default; negative rejected.
3233
responsetime:
3334
enabled: false # Opt-in X-Response-Time header (per-request processing time); off by default — OTel provides richer latency telemetry. SERVER_RESPONSETIME_ENABLED=true to restore.
3435

config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ func loadDefaults(k *koanf.Koanf) error {
331331
"server.path.health": "/health",
332332
"server.path.ready": "/ready",
333333
"server.gzip.minlength": 1024,
334+
"server.bodylimit": DefaultBodyLimitBytes,
334335

335336
// Database defaults not provided for deterministic behavior
336337
// Database will only be enabled when explicitly configured

config/config_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ func TestLoadWithDefaults(t *testing.T) {
5151
assert.Equal(t, 5*time.Second, cfg.Server.Timeout.Middleware)
5252
assert.Equal(t, 10*time.Second, cfg.Server.Timeout.Shutdown)
5353
assert.Equal(t, 1024, cfg.Server.Gzip.MinLength)
54+
assert.Equal(t, int64(10*1024*1024), cfg.Server.BodyLimit)
5455
assert.False(t, cfg.Server.ResponseTime.Enabled, "X-Response-Time header must default to opt-out")
5556

5657
// Database should be disabled by default (no defaults provided)
@@ -363,6 +364,7 @@ func TestLoadDefaultsInternalFunction(t *testing.T) {
363364
assert.Equal(t, "60s", k.String("server.timeout.idle"))
364365
assert.Equal(t, "5s", k.String("server.timeout.middleware"))
365366
assert.Equal(t, "10s", k.String("server.timeout.shutdown"))
367+
assert.Equal(t, int64(10*1024*1024), k.Int64("server.bodylimit"))
366368

367369
// Database defaults should NOT be provided
368370
assert.Equal(t, "", k.String("database.type"))
@@ -786,7 +788,7 @@ func clearEnvironmentVariables() {
786788
"SERVER_HOST", "SERVER_PORT", "SERVER_TIMEOUT_READ", "SERVER_TIMEOUT_WRITE",
787789
"SERVER_TIMEOUT_IDLE", "SERVER_TIMEOUT_MIDDLEWARE", "SERVER_TIMEOUT_SHUTDOWN",
788790
"SERVER_PATH_BASE", "SERVER_PATH_HEALTH", "SERVER_PATH_READY", "SERVER_GZIP_MINLENGTH",
789-
"SERVER_RESPONSETIME_ENABLED",
791+
"SERVER_BODYLIMIT", "SERVER_RESPONSETIME_ENABLED",
790792
"DATABASE_TYPE", "DATABASE_HOST", "DATABASE_PORT", testDatabaseDatabase,
791793
testDatabaseUsername, "DATABASE_PASSWORD", "DATABASE_TLS_MODE",
792794
testDatabaseMaxConns, "DATABASE_POOL_IDLE_CONNECTIONS",

config/types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ type ServerConfig struct {
9494
Path PathConfig `koanf:"path" json:"path" yaml:"path" toml:"path" mapstructure:"path"`
9595
Gzip GzipConfig `koanf:"gzip" json:"gzip" yaml:"gzip" toml:"gzip" mapstructure:"gzip"`
9696

97+
// BodyLimit is the maximum request body size in bytes. A value of 0 resolves
98+
// to the framework default (10 MB) at wire-up; a negative value is rejected by
99+
// config validation.
100+
BodyLimit int64 `koanf:"bodylimit" json:"bodylimit" yaml:"bodylimit" toml:"bodylimit" mapstructure:"bodylimit"`
101+
97102
ResponseTime ResponseTimeConfig `koanf:"responsetime" json:"responsetime" yaml:"responsetime" toml:"responsetime" mapstructure:"responsetime"`
98103

99104
// LogRoutes toggles the per-route "Route registered" startup log lines

config/validation.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ const (
3636
// Connection layers compare against this constant to decide whether to
3737
// apply per-connection timezone setup.
3838
TimezoneDisabledSentinel = "-"
39+
40+
// DefaultBodyLimitBytes is the maximum request body size (10 MB) applied when
41+
// server.bodylimit is unset or resolves to a non-positive value. Single source
42+
// of truth for both the koanf default (loadDefaults) and the server-side
43+
// wire-up fallback (server.SetupMiddlewares).
44+
DefaultBodyLimitBytes int64 = 10 * 1024 * 1024
3945
)
4046

4147
// Messaging reconnection defaults
@@ -305,6 +311,13 @@ func validateServer(cfg *ServerConfig) error {
305311
return NewValidationError("server.gzip.minlength", errMustBeNonNegative)
306312
}
307313

314+
// A negative body limit is an operator error (a typo silently reverting to the
315+
// default is worse than a startup failure). Zero is permitted and resolves to
316+
// the framework default at wire-up (see server.SetupMiddlewares).
317+
if cfg.BodyLimit < 0 {
318+
return NewValidationError("server.bodylimit", errMustBeNonNegative)
319+
}
320+
308321
return nil
309322
}
310323

config/validation_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,20 @@ func TestValidateServerFailures(t *testing.T) {
447447
},
448448
expectedError: "server.gzip.minlength",
449449
},
450+
{
451+
name: "negative_bodylimit",
452+
cfg: ServerConfig{
453+
Port: 8080,
454+
Timeout: TimeoutConfig{
455+
Read: 15 * time.Second,
456+
Write: 30 * time.Second,
457+
Middleware: 5 * time.Second,
458+
Shutdown: 10 * time.Second,
459+
},
460+
BodyLimit: -1,
461+
},
462+
expectedError: "server.bodylimit",
463+
},
450464
}
451465

452466
for _, tt := range tests {

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ require (
2121
github.com/knadh/koanf/providers/file v1.2.1
2222
github.com/knadh/koanf/providers/rawbytes v1.0.0
2323
github.com/knadh/koanf/v2 v2.3.5
24-
github.com/labstack/echo-opentelemetry v0.0.2
25-
github.com/labstack/echo/v5 v5.2.1
24+
github.com/labstack/echo-opentelemetry v0.0.3
25+
github.com/labstack/echo/v5 v5.3.0
2626
github.com/rabbitmq/amqp091-go v1.12.0
2727
github.com/redis/go-redis/v9 v9.21.0
2828
github.com/rs/zerolog v1.35.1

go.sum

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,10 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
116116
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
117117
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
118118
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
119-
github.com/labstack/echo-opentelemetry v0.0.2 h1:zNzIDYf2uXSYgpBXcuwRELrnOOFSRvMaC41DTF80ke8=
120-
github.com/labstack/echo-opentelemetry v0.0.2/go.mod h1:kBwoqFuXPxpM9fxbs++asMsI42uOufQjuYJut3qqg6w=
121-
github.com/labstack/echo/v5 v5.2.1 h1:TzpIksY6zLMzV0T0ycYbvTEoj9w6o6AcL5twg182VTY=
122-
github.com/labstack/echo/v5 v5.2.1/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo=
119+
github.com/labstack/echo-opentelemetry v0.0.3 h1:r4HAlV3PniSlUmsqpXQ+YwFxo9RNuEOm2bGU9hAOhUY=
120+
github.com/labstack/echo-opentelemetry v0.0.3/go.mod h1:gGPMgJmQkDreqWyEzfPccPhaT8B3XLag2AByizhNoM4=
121+
github.com/labstack/echo/v5 v5.3.0 h1:KT74Mprk053PQEHwSZdeCDIz1BigTZOZhavMD0c9Fjs=
122+
github.com/labstack/echo/v5 v5.3.0/go.mod h1:Q3j2+clBRgJr0O3DDONQeXNsM7RHgSwUhcuo47unqm8=
123123
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
124124
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
125125
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
@@ -218,8 +218,8 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG
218218
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
219219
go.opentelemetry.io/contrib/instrumentation/runtime v0.69.0 h1:MtkMsuRo3zEXTTMALfyrszwCDZTkB6wolyPjbwFAdq0=
220220
go.opentelemetry.io/contrib/instrumentation/runtime v0.69.0/go.mod h1:FYTxnpsm+UPD0erZNq20GvnM8T2YQHiHtT2vokdpoac=
221-
go.opentelemetry.io/contrib/propagators/b3 v1.39.0 h1:PI7pt9pkSnimWcp5sQhUA9OzLbc3Ba4sL+VEUTNsxrk=
222-
go.opentelemetry.io/contrib/propagators/b3 v1.39.0/go.mod h1:5gV/EzPnfYIwjzj+6y8tbGW2PKWhcsz5e/7twptRVQY=
221+
go.opentelemetry.io/contrib/propagators/b3 v1.42.0 h1:B2Pew5ufEtgkjLF+tSkXjgYZXQr9m7aCm1wLKB0URbU=
222+
go.opentelemetry.io/contrib/propagators/b3 v1.42.0/go.mod h1:iPgUcSEF5DORW6+yNbdw/YevUy+QqJ508ncjhrRSCjc=
223223
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
224224
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
225225
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.20.0 h1:rydZ9sxbcFdm/oWrVyfLTjHIygMgv0bEeMd+3B/BvoM=

renovate.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@
44
"config:recommended"
55
],
66
"postUpdateOptions": ["gomodTidy"],
7+
"packageRules": [
8+
{
9+
"description": "Keep the echo/v5 engine and its OpenTelemetry companion on one PR so the engine and its instrumentation always update together.",
10+
"matchDatasources": ["go"],
11+
"matchPackageNames": [
12+
"github.com/labstack/echo/v5",
13+
"github.com/labstack/echo-opentelemetry"
14+
],
15+
"groupName": "echo (engine + otel)"
16+
}
17+
],
718
"customManagers": [
819
{
920
"customType": "regex",

server/constants.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const (
1111
errCodeConflict = "CONFLICT"
1212
errCodeTooManyRequests = "TOO_MANY_REQUESTS"
1313
errCodeServiceUnavailable = "SERVICE_UNAVAILABLE"
14+
errCodeMethodNotAllowed = "METHOD_NOT_ALLOWED"
1415
errCodeInternalError = "INTERNAL_ERROR"
1516
)
1617

0 commit comments

Comments
 (0)