Skip to content

Investigate: whether App should implement http.Handler #109

Description

@pkkummermo

Problem

*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).

  • Path cleaning. http.ServeMux currently canonicalizes for free: //foo and /a/../b are
    301'd before rootHandlerFunc ever sees them. Removing the mux removes that, silently. Decide
    whether to reimplement it and under what defaults — gin carries explicit RedirectTrailingSlash
    (default on) and RedirectFixedPath (default off) fields for precisely this reason. Adjacent to
    the 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 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions