You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
*App is not an http.Handler. Start is the only way to serve a request: it binds the listener,
owns the http.Server, and registers rootHandlerFunc on an internal http.ServeMux
(server.go:277). The router itself is unexported and reachable only from inside the package.
This surfaced during perf work. bench_test.go and perf_test.go drive requests by calling app.rootHandlerFunc directly, which only compiles because they live in package govalin — the
allocation budgets in ADR 0008 measure something no consumer can reach. Among net/http-based Go
frameworks this position is unusual: gin, echo and chi all make their router the handler, and the
exception (fiber) is only an exception because it is on fasthttp.
This is an investigation, not a decision already taken. The output is an ADR with a recommendation.
Scope
Questions to answer:
What it actually buys. Four candidate wins; each needs confirming and sizing rather than
assuming:
Drops App.mux from the hot path. Every request currently goes through a ServeMux lookup
before reaching govalin's own routing, and the mux exists solely to route / to rootHandlerFunc. Measurable against the ADR 0008 budgets.
The budgets stop depending on package-internal access, and consumers gain the same httptest.NewRecorder() measurement path for their own apps.
govalintest collapses to httptest.NewServer(app): no port, no readiness channel, no 5s
timeout, no CloseIdleConnections-before-Shutdown workaround. ADR 0004's startup-event
readiness exists to solve a problem that would stop existing.
Composition, likely the largest external win: mounting under another mux or StripPrefix, func(http.Handler) http.Handler middleware (otel, gzip, prometheus), a user-owned http.Server for TLS/h2c/unix sockets, serverless adapters.
When setup happens.Start applies plugins (plugin.Apply(app)) and registers the mux route;
a request arriving through ServeHTTP reaches neither. Needs a defined "serving has begun" point
both doors hit. Gin's answer is a sync.Once guard at the top of ServeHTTP plus an eager call in Run. This interacts with route and Events registration being deliberately unsynchronized —
see the warning contract on App.Events (server.go:60).
What Start and Port() mean afterwards.Start presumably stays the front door and becomes
a thin wrapper over http.Server{Handler: app}. Port() has no answer when the app is served as
a handler, and ADR 0004's readiness contract is built on it — decide what both mean.
API shape.ServeHTTP on *App alone, or also a Handler() accessor (gin has one so h2c
wrapping has somewhere to live)? Adding the method widens *App's public surface; check whether
that breaks anything for consumers.
Notes
Prompted by the perf/per-request-constant work — the mux hop is a per-request cost that branch is
otherwise trying to shave, so the perf answer and the API answer are the same answer.
Reference points in gin v1.12.0: ServeHTTP at gin.go:662, Run at gin.go:540, the routeTreesUpdated sync.Once at gin.go:97, the redirect options at gin.go:99-115.
Problem
*Appis not anhttp.Handler.Startis the only way to serve a request: it binds the listener,owns the
http.Server, and registersrootHandlerFuncon an internalhttp.ServeMux(
server.go:277). The router itself is unexported and reachable only from inside the package.This surfaced during perf work.
bench_test.goandperf_test.godrive requests by callingapp.rootHandlerFuncdirectly, which only compiles because they live in packagegovalin— theallocation budgets in ADR 0008 measure something no consumer can reach. Among net/http-based Go
frameworks this position is unusual: gin, echo and chi all make their router the handler, and the
exception (fiber) is only an exception because it is on fasthttp.
This is an investigation, not a decision already taken. The output is an ADR with a recommendation.
Scope
Questions to answer:
What it actually buys. Four candidate wins; each needs confirming and sizing rather than
assuming:
App.muxfrom the hot path. Every request currently goes through aServeMuxlookupbefore reaching govalin's own routing, and the mux exists solely to route
/torootHandlerFunc. Measurable against the ADR 0008 budgets.httptest.NewRecorder()measurement path for their own apps.govalintestcollapses tohttptest.NewServer(app): no port, no readiness channel, no 5stimeout, no
CloseIdleConnections-before-Shutdownworkaround. ADR 0004's startup-eventreadiness exists to solve a problem that would stop existing.
StripPrefix,func(http.Handler) http.Handlermiddleware (otel, gzip, prometheus), a user-ownedhttp.Serverfor TLS/h2c/unix sockets, serverless adapters.When setup happens.
Startapplies plugins (plugin.Apply(app)) and registers the mux route;a request arriving through
ServeHTTPreaches neither. Needs a defined "serving has begun" pointboth doors hit. Gin's answer is a
sync.Onceguard at the top ofServeHTTPplus an eager call inRun. This interacts with route andEventsregistration being deliberately unsynchronized —see the warning contract on
App.Events(server.go:60).Path cleaning.
http.ServeMuxcurrently canonicalizes for free://fooand/a/../bare301'd before
rootHandlerFuncever sees them. Removing the mux removes that, silently. Decidewhether to reimplement it and under what defaults — gin carries explicit
RedirectTrailingSlash(default on) and
RedirectFixedPath(default off) fields for precisely this reason. Adjacent tothe trailing-slash matching already settled in A route registered with a trailing slash never matches the slashless form #87 and A trailing slash below a static mount 404s, making directory listings unreachable #85.
What
StartandPort()mean afterwards.Startpresumably stays the front door and becomesa thin wrapper over
http.Server{Handler: app}.Port()has no answer when the app is served asa handler, and ADR 0004's readiness contract is built on it — decide what both mean.
API shape.
ServeHTTPon*Appalone, or also aHandler()accessor (gin has one so h2cwrapping has somewhere to live)? Adding the method widens
*App's public surface; check whetherthat breaks anything for consumers.
Notes
Prompted by the
perf/per-request-constantwork — the mux hop is a per-request cost that branch isotherwise trying to shave, so the perf answer and the API answer are the same answer.
Reference points in gin v1.12.0:
ServeHTTPatgin.go:662,Runatgin.go:540, therouteTreesUpdated sync.Onceatgin.go:97, the redirect options atgin.go:99-115.