labstack/echo
github.com/labstack/echo ↗ · 139 files · 1.4 MB
Echo High performance, extensible, minimalist Go web framework.
Overview
Echo v5 is a Go HTTP framework for developers building APIs, web applications, static-file services, and reverse proxies. It combines a radix-tree router, pooled request contexts, configurable binding and serialization, centralized error handling, and composable middleware around Go’s net/http interfaces. This brief investigates commit 99c395d846e079d4cffd29de9298d1dd829e8b04 through public source reads, including startup, request processing, static-file serving, proxying, and rate-limit state; no repository code or tests were executed.
System map
The repository provides an embeddable HTTP framework, with application startup illustrated in the README. The core package owns routing, request contexts, binding, responses, and server helpers; middleware supplies optional request policies and transports. State examined here consists of route registries, reusable pools, request-local maps, and an in-memory rate limiter. Deployment remains the consuming application's responsibility through net/http or StartConfig.
- go.mod declares github.com/labstack/echo/v5 and Go 1.25.0, with direct requirements on testify, x/net, and x/time; the README example imports the library, registers GET /, and starts an application-owned server.
- Echo aggregates replaceable Binder, Renderer, Validator, JSONSerializer, IPExtractor, SchemeExtractor, HTTPErrorHandler, and Router components, alongside route hooks, slog logging, middleware chains, and a context pool.
- New installs DefaultBinder, DefaultJSONSerializer, a working-directory filesystem, an overwrite-enabled DefaultRouter, DefaultHTTPErrorHandler(false), and a context-pool factory before compiling dispatch chains.
- DefaultRouter owns one radix-tree root whose nodes carry method handlers, plus a Routes registry; Add parses patterns and inserts static, parameter, and wildcard nodes. This differs from CLAUDE.md's description of separate trees per method.
- Context owns the request, response wrappers, cached query values, route metadata, path values, handler, logger, and request-local store. Reset clears these transient values while retaining the store allocation and path-value capacity.
- Global and pre-routing middleware chains are compiled by buildRouterChains; route middleware is compiled during DefaultRouter.Add. Group.AddRoute combines group middleware with route definitions before registration.
- StartConfig creates a standard http.Server and listener, supports custom listeners and TLS configuration, and coordinates context-triggered graceful shutdown. StartTLS loads certificate inputs through fs.FS or accepts bytes, then defaults to TLS 1.2 minimum and h2 negotiation.
- Development automation includes Makefile lint/vet/race targets, a CI race-and-coverage matrix, and a separate checks workflow for golint, staticcheck, and govulncheck. The Makefile's unversioned package prefix differs from the /v5 module declaration.
Feature catalog
Echo exposes developer-facing APIs rather than a fixed collection of application screens or business routes. Its features cover routing, input conversion, output formats, static content, security policies, traffic management, and observability. Middleware is explicitly installed by consuming applications, and configuration factories allow customization. The catalog below groups the inspected capabilities; the investigation is bounded rather than exhaustive.
- Routing supports HTTP method helpers including QUERY, custom method registration, Any/Match, fallback routes, nested groups, and named route reversal. NewVirtualHostHandler dispatches exact request Host matches to separate Echo instances, falling back to its own routes.
- Input APIs bind path, query, header, JSON, XML, URL-encoded form, and multipart data. ValueBinder provides fluent source-specific conversion with fail-fast or accumulated errors, while generic PathParam/QueryParam helpers distinguish missing values from empty values.
- Context supports HTML, text, JSON, validated JSONP, XML, blobs, streaming, attachments, inline files, redirects, and empty responses; Render delegates to a configured template renderer. Error output can use the default JSON handler or RFC 9457 Problem Details.
- Static and StaticFS register filesystem-backed GET routes; File/FileFS expose individual files. Static middleware additionally offers configurable index files, directory browsing, group-base handling, and HTML5 index fallback restricted to router-level 404 errors.
- Security middleware includes callback-driven BasicAuth and KeyAuth, CSRF protection using fetch metadata or cookie/token comparison, configured CORS origins, and Secure headers such as CSP and HSTS. CORS rejects wildcard origins with credentials unless custom origin logic is supplied.
- Traffic APIs provide token-bucket rate limiting with custom stores and response headers, reverse proxying with random or round-robin balancing, retries, rewrites, custom transports, response modification, and WebSocket forwarding. StartConfig supplies HTTP/TLS listener and shutdown controls.
- Request transformation middleware includes method override, wildcard/regex rewrites, trailing-slash normalization, and HTTPS/www redirects. Payload middleware supplies configurable gzip responses, gzip request decompression with a default 100 MiB expansion limit, BodyLimit, and cooperative ContextTimeout handling.middleware/method_override.go#L35-L78middleware/rewrite.go#L14-L34middleware/slash.go#L25-L76middleware/slash.go#L102-L141middleware/redirect.go#L33-L80middleware/redirect.go#L104-L145middleware/compress.go#L24-L65middleware/decompress.go#L24-L62middleware/body_limit.go#L49-L76middleware/context_timeout.go#L38-L70
- RequestLogger offers selectable request/response fields and a default slog callback; RequestID propagates or generates an ID in response headers. BodyDump calls an application callback with payloads and handler errors, while Recover converts ordinary panics into returned errors and rethrows http.ErrAbortHandler.
Data flow
Incoming net/http requests enter a pooled Context and acquire route metadata before reaching the selected handler chain. Parsing and binding move request-controlled values into application-owned targets, while serializers and response wrappers govern output commitment. File serving reads from configured filesystems and proxying crosses an upstream network boundary. Request-local state is cleared between uses; rate-limit visitor state survives requests only within its store instance.
- ServeHTTP delegates to serveHTTPFunc; the default serveHTTP obtains a Context from sync.Pool, resets it with the incoming request/writer, resolves a handler directly or through preChain, and returns the Context to the pool after error handling.
- DefaultRouter.Route selects its matching path from URL.Path and conditionally RawPath, captures parameter values, resolves matched or fallback handlers, and writes RouteInfo, PathValues, Context.Path, and request.Pattern before returning the handler.
- QueryParam scans RawQuery without creating a map unless QueryParams has already populated the cache. It decodes percent escapes and plus signs, skips malformed or semicolon-containing pairs, and returns the first valid matching value.
- Context.Bind delegates to Echo.Binder. DefaultBinder applies path values, optionally query values for GET/DELETE/HEAD/QUERY, then Content-Type-driven body binding; later sources can overwrite earlier values, and header binding is a separate operation.
- DefaultJSONSerializer.Deserialize reads the complete body into a reset pooled buffer, calls json.Unmarshal, and retains only buffers at or below 64 KiB capacity. Status-coded read errors survive; other read or decode failures become wrapped 400 errors.
- Context.JSON installs a delayedStatusWriter around the current response and restores it afterward; serialization's first write commits the proposed status. Response then records commitment, byte count, and before/after callbacks, while the default error handler skips already committed responses.
- StaticDirectoryHandler validates request and wildcard paths before optional unescaping, checks the configured filesystem, redirects directory URLs lacking a slash, and delegates to fsFile. fsFile opens content or index.html and requires io.ReadSeeker for http.ServeContent.
- RateLimiter extracts an identifier and delegates to AllowContext when available, otherwise Allow. Its memory store updates a mutex-protected visitor map and token bucket across requests, periodically deleting stale entries; Context.Set data is separately cleared by Reset.
Behavior traces
The following traces follow source execution order through concrete framework flows. Startup proceeds from construction and registration into listener creation and shutdown coordination. A bound API request proceeds through dispatch, input conversion, and output serialization. Static-file delivery, proxy requests, and rate-limit decisions expose additional filesystem, network, and state boundaries.
- Startup configuration: New → NewRouter → contextPool.New installation → buildRouterChains; GET → Add → Echo.add → optional OnAddRoute → Router.Add. Registration also raises contextPathParamAllocSize when a route needs more parameters.
- Server startup and stop: Echo.Start → signal.NotifyContext → StartConfig.Start → start → Listen → optional tls.NewListener → BeforeServeFunc → server.Serve. Context cancellation triggers gracefulShutdown → server.Shutdown with a default ten-second timeout; http.ErrServerClosed is suppressed.
- Request dispatch: ServeHTTP → serveHTTP → Context.Reset → preChain when registered → Router.Route → global chain → dispatch → route handler. applyMiddleware wraps in reverse registration order so the first registered middleware enters first; returned errors reach HTTPErrorHandler.
- Bound API input: handler calls Context.Bind → DefaultBinder.Bind → BindPathValues → method-conditional BindQueryParams → BindBody. JSON bodies call JSONSerializer.Deserialize; unsupported nonempty body media types return 415. Validation occurs only if the handler separately calls Context.Validate.
- API output/error: Context.JSON → json → DefaultJSONSerializer.Serialize → delayedStatusWriter.Write → Response.WriteHeader/Write. On a returned error, DefaultHTTPErrorHandler checks commitment, extracts HTTPStatusCoder, chooses JSON payload shape, and sends NoContent instead of JSON for HEAD.
- Static file delivery: Echo.Static → MustSubFS → GET wildcard registration → StaticDirectoryHandler. At request time the handler rejects unclean paths, optionally unescapes and rechecks them, runs fs.Stat, redirects directories when needed, then fsFile opens the file/index and calls http.ServeContent.
- Proxy action: ProxyConfig.ToMiddleware → rewriteURL → overwrite IP/scheme headers → Balancer.Next → store selected target → proxyHTTP or proxyRaw. HTTP errors are stored under _error; retries require remaining RetryCount and RetryFilter approval, while missing targets immediately return 502.
- Rate-limit state: RateLimiter middleware → IdentifierExtractor → RateLimiterMemoryStore.AllowContext → allow → lookup/create Visitor → update lastSeen → optional cleanupStaleVisitors → AllowN → setRateLimitHeaders. Denial invokes DenyHandler, which defaults to a wrapped 429; the visitor map is process-local memory.
Testing signals
Tests and CI were inspected, not executed; their existence establishes intended checks, not passing results. The examined tests use Go testing, httptest, testify assertions, and filesystem fixtures. They target dispatch/pool invariants, JSON parsing, static security regressions, concurrency, lifecycle behavior, proxying, and rate-limit headers. Inspection is bounded and does not establish exhaustive coverage, deployment correctness, or measured performance.
- Tests use testing, httptest, and testify/assert; echotest.ContextConfig constructs request bodies, headers, route metadata, and recorders. ServeWithHandler invokes a handler directly and applies an error handler, so that helper alone does not exercise router/global middleware dispatch.
- CI specifies Go 1.25, 1.26, and 1.27 on Ubuntu, macOS, and Windows, running go test -race with atomic coverage. Coverage uploads are limited to the latest configured Go on Ubuntu and do not fail CI on upload errors; Codecov config permits a one-percent status threshold.
- The checks workflow installs and runs golint, staticcheck, and govulncheck using @latest. Separately, .golangci.yaml enables revive, disables errcheck, and excludes test files; these workflow steps do not invoke golangci-lint.
- dispatch_pool_test.go guards store clearing, JSON status across Reset, nested JSON writer behavior, and Pre/Use execution for 404, 405, and OPTIONS. Query fast-path tests compare malformed escapes, repeated keys, empty values, Unicode, and semicolon segments against url.ParseQuery.
- JSON tests reject trailing objects/garbage, alternate large and small payloads, exercise pooled decoding in 64 goroutines, decode a body above the pool-retention cap, and check read failures. These inspect parser/pool behavior rather than proving endpoint request-size limits.
- Static regression tests combine guarded routes with fstest.MapFS and assert no protected content appears for encoded slash, backslash, or double-encoding variants. Group-mounted StaticFS receives separate encoded-separator cases; these fixtures do not establish behavior for every custom filesystem.
- Inspected tests include concurrent route metadata reads during additions, graceful-shutdown completion/deadline branches, and preference for AllowContext plus 429/rate-limit headers. These are concrete concurrency/lifecycle contracts, with no execution result supplied by this investigation.
- Proxy tests arrange reachable and unreachable targets plus retry filters and a WebSocket echo exchange; CSRF tests cover fetch-metadata allow/block branches. PR benchmark CI compares base/current checkouts with eight 100 ms samples and advisory benchstat output, which provides no measured speedup in this report.
Risk map
These findings describe source-level boundaries and fragile behavior, not confirmed vulnerabilities. Most security decisions are configured by applications through middleware, validators, trust policies, and filesystem roots. Pool reuse and response commitment couple changes in dispatch, serialization, logging, and error handling. One connection-lifecycle concern is explicitly a hypothesis requiring focused verification.
- Static serving defaults to no path unescaping and rejects dot/empty segments, but explicitly enabling decoding can still make encoded separators resolve beyond route-based guards. Filesystem root selection is also significant: defaultFS sub-root construction accepts configured absolute or parent-relative roots outside the initial working directory.
- JSON deserialization buffers the full body before decoding; the 64 KiB cap only controls pool retention. Request-size enforcement depends on separately installed BodyLimit or MaxBytesReader, while decompression has its own configurable expansion limit that can be disabled.
- BodyDump's request cap changes downstream input: it reads only the prefix, drains the remainder, and replaces Request.Body with the captured prefix. TestBodyDump_RequestExceedsLimit explicitly expects this truncation; payload callbacks also receive raw content that application logging must handle appropriately.
- Forwarded identity and scheme depend on trust configuration: direct-peer IP is the default, but LegacyIPExtractor and LegacySchemeExtractor retain header-trusting behavior. Scheme extraction trusts private/loopback/link-local peers by default and requires trusted proxies to overwrite forwarding headers.
- BasicAuth and KeyAuth delegate secure credential comparison to application validators. KeyAuth can deliberately continue after rejected/missing credentials when ErrorHandler returns nil and ContinueOnIgnoredError is enabled, so downstream authorization must account for that configured public-access branch.
- Echo fields and DefaultRouter mutations are documented as unsafe during serving; concurrentRouter locks router operations only. serveHTTP returns Context to the pool after processing, and Reset clears its state, making escaped Context references and arbitrary runtime configuration changes important review boundaries.
- Hypothesis: proxyRaw may retain hijacked connections when one io.Copy direction finishes but the other remains blocked, because it waits for both results before deferred connection closes run. The inspected WebSocket test verifies a normal echo exchange rather than this asymmetric-close scenario. (inferred)
- Onboarding/tooling descriptions drift from implementation: CLAUDE.md lists separate method trees and bundled JWT middleware, README lists JWT externally, and Makefile uses an unversioned package prefix. These discrepancies can misdirect edits or checks; no Makefile failure was executed or confirmed.
Extension points
Extension is primarily through Go interfaces, handler factories, and callbacks. Router replacement has an explicit context-population contract, while middleware factories offer configurable request policies. Binding, rendering, serialization, errors, proxying, and rate limiting each expose narrower integration boundaries. There is no need to alter the core dispatch path for most application-specific behavior described here.
- Config.Router accepts implementations of Add, Remove, Routes, and Route; Route must initialize Context route/path values. NewConcurrentRouter wraps an implementation with read/write locks and returns cloned route metadata for callers.
- Route carries method, path, name, handler, and middleware, and AddRoute returns registration errors rather than panicking like Add. OnAddRoute runs before Router.Add and can reject registration; RouteInfo and Routes provide reversal and metadata lookup.
- MiddlewareFunc transforms HandlerFunc, and MiddlewareConfigurator.ToMiddleware can return configuration errors. Applications can attach middleware through Pre, Use, groups, or routes; middleware development guidance recommends returning request errors through the chain.
- Binder.Bind is replaceable, BindUnmarshaler supports custom parameter conversion, and ValueBinder exposes ValueFunc, ValuesFunc, ErrorFunc, and CustomFunc. Reflection metadata caches explicitly couple the four source tags to tagName and bindMetaFor.
- Config allows custom JSONSerializer, Renderer, and Validator implementations. Context delegates Bind/Validate/Render to configured services, reports absent validator/renderer registrations, and buffers rendered templates before sending output.
- HTTPErrorHandler is an assignable callback; the default recognizes HTTPStatusCoder and status-bearing json.Marshaler implementations. ProblemDetailsHTTPErrorHandler additionally accepts ProblemErrorer, while BindingError.MarshalJSON supplies structured field/message output.
- ProxyBalancer exposes AddTarget, RemoveTarget, and Next(Context); ProxyConfig accepts RetryFilter, ErrorHandler, Transport, ModifyResponse, and rewrite maps. Next can replace the Context request, which proxy execution rereads before upstream forwarding.
- RateLimiterStore supplies Allow, with optional AllowContext for response-aware decisions; IdentifierExtractor and denial/error handlers are configurable. IPExtractor and SchemeExtractor separately let applications adapt request identity and transport interpretation to their proxy topology.
Contributor handoff
Begin with implementation and nearby tests, then use documentation to understand intended public compatibility. Tests and CI were inspected, not executed; future executable verification requires authorization outside this read-only investigation. Keep edits focused on the relevant interface or middleware and preserve cross-file dispatch, pooling, path, and response invariants. The inventory and test inspection are bounded, and risk observations must not be promoted into confirmed findings without verification.
- Use the /v5 import path and pointer Context handlers, with slog logging and Context-first callback signatures. The migration document describes these API changes, and current HandlerFunc/Binder declarations substantiate them.
- For dispatch changes, read New, buildRouterChains, serveHTTP, Context.Reset, and dispatch_pool_test.go together. Preserve Pre-before-routing, Use-after-routing, fallback-handler middleware execution, store clearing, and nested JSON writer behavior.
- For route syntax changes, inspect route_path.go, DefaultRouter.Add/Route, and RouteInfo.Reverse together: walkRoutePath is shared by registration and URL reversal, including escaped colons and inline verbs.
- For static-file changes, review both core StaticDirectoryHandler and Static middleware, plus encoded-separator regressions. Preserve path validation before and after optional decoding and the distinction between router-level 404 fallback and errors from matched handlers.
- For binding changes, preserve source precedence and method restrictions, explicit header binding, and separation of Bind from Validate. Changes to reflection source tags require synchronized metadata fields, tagName dispatch, and bindMetaFor extraction.
- For response wrappers, implement Unwrap so UnwrapResponse can reach Echo.Response, preserve delayed JSON commitment, and account for RequestLogger's default HandleError=true committing errors before outer middleware returns. Avoid assuming a returned error can replace an already sent response.
- If later execution is authorized, start with targeted tests in the changed package, then expand to package tests and go test -race ./... as appropriate. CI's inspected baseline uses race detection and atomic coverage; verify Makefile's package discovery before trusting make test or make race.
- Prefer config.ToMiddleware for new middleware validation and return request errors rather than swallowing them. Before altering BodyDump or proxyRaw, read their current downstream-input and connection-ownership behavior plus relevant tests; existing assertions describe intended behavior, not results of this investigation.
Languages
| Language | Files | Share of source |
|---|---|---|
| Go | 108 | 100% |
| Markdown | 10 | docs/config |
| YAML | 5 | docs/config |
| HTML | 2 | docs/config |
| JSON | 2 | docs/config |
Start reading here
Probable source entrypoint: go.mod — inferred from the directory tree and manifest, not the upstream docs.
Source roots
middleware/ (53 files) echotest/ (7 files) _fixture/ (6 files)
Source layout — middleware/
| Path | Files |
|---|---|
middleware/testdata/ | 1 |
middleware/DEVELOPMENT.md | 1 |
middleware/basic_auth.go | 1 |
middleware/basic_auth_test.go | 1 |
middleware/body_dump.go | 1 |
middleware/body_dump_test.go | 1 |
middleware/body_limit.go | 1 |
middleware/body_limit_test.go | 1 |
middleware/compress.go | 1 |
middleware/compress_test.go | 1 |
middleware/context_timeout.go | 1 |
middleware/context_timeout_test.go | 1 |
middleware/cors.go | 1 |
middleware/cors_test.go | 1 |
Repository structure
| Top-level path | Files |
|---|---|
middleware/ | 53 |
echotest/ | 7 |
_fixture/ | 6 |
.github/ | 4 |
Docs found: CLAUDE.mdREADME.md
Package
go.mod — github.com/labstack/echo/v5, 0 declared dependencies.
CI & config entrypoints
README.md— Repository readmeCLAUDE.md— Claude instructions.github/workflows/checks.yml— GitHub Actions workflow.github/workflows/ci.yml— GitHub Actions workflow
Structural health signals
Average score 7.5/10 across 96 analyzed source files; 19 hotspots surfaced.
router_test.go— 1544 lines, score 5.2 (Large file, Long function/block)echo_test.go— 1680 lines, score 5.2 (Large file, Long function/block)context_test.go— 1695 lines, score 5.2 (Large file, Long function/block)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
99c395d846e02026-10-01 — ci: rename the test workflow to ci.yml (#3141)e6b10d9e8f132026-10-01 — ci: keep the Go build cache current (#3139)680772f742b82026-10-01 — ci: run benchmark comparison on pull requests only, and shorten it (#3140)af07b69b44c02026-09-29 — fix(router): share route syntax across routing operations (#3133)eadbef28df3b2026-09-30 — fix(router): keep escaped colon and parameter routes reachable (#3113)