Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,22 @@ jobs:
cache-dependency-path: |
go.mod
go.sum
docs/go.mod
docs/go.sum
examples/go.mod
examples/go.sum

- name: Verify module files are tidy
run: |
go mod tidy -diff
cd examples
GOWORK=off go mod tidy -diff
(cd docs && GOWORK=off go mod tidy -diff)
(cd examples && GOWORK=off go mod tidy -diff)

- name: Download dependencies
run: |
go mod download
cd examples
GOWORK=off go mod download
(cd docs && GOWORK=off go mod download)
(cd examples && GOWORK=off go mod download)

- name: Run root tests
run: go test ./... -v
Expand All @@ -45,6 +47,18 @@ jobs:
cd examples
GOWORK=off go test ./... -v

- name: Verify documentation tooling
run: |
cd docs
GOWORK=off go test ./... -v
GOWORK=off go test -tags=benchrender ./... -v

- name: Verify benchmark rendering matches the committed snapshot
run: |
make benchmark-svg-verify
git diff --exit-code -- README.md docs/bench/benchmarks_rows.json docs/bench/framework_comparison.svg
test -z "$(git ls-files --others --exclude-standard -- docs/bench/benchmarks_rows.json docs/bench/framework_comparison.svg)"

race:
runs-on: ubuntu-latest

Expand Down
17 changes: 17 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
.PHONY: bench bench-frameworks benchmark-svg benchmark-svg-preview benchmark-svg-render benchmark-svg-verify test

bench:
GOCACHE=/tmp/gocache GOMODCACHE=/tmp/gomodcache go test ./adapter/echoweb -run '^$$' -bench 'Benchmark(Echo|Web)(PlainText|ParamsJSON|MiddlewareChain|MiddlewareChainSingleUse|GroupAndRouteMiddleware|Compress|BodyDump|WebSocketJSON|WebSocketJSONPersistent)$$' -benchmem

bench-frameworks:
cd docs && GOWORK=off GOCACHE=/tmp/gocache GOMODCACHE=/tmp/gomodcache go test ./bench -run '^$$' -bench '^BenchmarkHTTPStacks$$' -benchmem -count=1 -cpu=1

benchmark-svg:
cd docs && BENCH_RENDER=1 BENCH_REQUIRE_CLEAN_SNAPSHOT=1 GOWORK=off GOCACHE=/tmp/gocache GOMODCACHE=/tmp/gomodcache go test -tags=benchrender ./bench -run '^TestRenderBenchmarks$$' -count=1 -v

benchmark-svg-preview:
cd docs && BENCH_RENDER=1 BENCH_ALLOW_DIRTY=1 GOWORK=off GOCACHE=/tmp/gocache GOMODCACHE=/tmp/gomodcache go test -tags=benchrender ./bench -run '^TestRenderBenchmarks$$' -count=1 -v

benchmark-svg-render:
cd docs && BENCH_RENDER=1 BENCH_RENDER_ONLY=1 GOWORK=off GOCACHE=/tmp/gocache GOMODCACHE=/tmp/gomodcache go test -tags=benchrender ./bench -run '^TestRenderBenchmarks$$' -count=1 -v

benchmark-svg-verify:
cd docs && BENCH_RENDER=1 BENCH_RENDER_ONLY=1 BENCH_REQUIRE_CLEAN_SNAPSHOT=1 GOWORK=off GOCACHE=/tmp/gocache GOMODCACHE=/tmp/gomodcache go test -tags=benchrender ./bench -run '^TestRenderBenchmarks$$' -count=1 -v

test:
GOCACHE=/tmp/gocache GOMODCACHE=/tmp/gomodcache go test ./...
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,28 @@ adapter.Echo().IPExtractor = echo.ExtractIPDirect()

Behind a trusted proxy, configure `echo.ExtractIPFromXFFHeader` or `echo.ExtractIPFromRealIPHeader` with trust options that match the deployment, and ensure the edge proxy removes client-supplied forwarding headers before adding its own.

## Performance

<!-- bench:embed:start -->
<p align="center">
<img src="docs/bench/framework_comparison.svg" alt="Go HTTP stack loopback and in-process performance comparison">
</p>

Whiskers in every panel show the observed sample minimum and maximum. The first panel measures single-core HTTP/1.1 loopback requests per second over a reused connection. The other panels measure in-process `ServeHTTP` operations per second, and their allocation figures cover the complete route and handler dispatch. Middleware details show the median paired latency added above the plaintext route measured in the same benchmark process. Each primary value is the median of 7 samples at `1s` with `GOMAXPROCS=1`.

Bars are scaled independently within each panel, and small differences should not be treated as rankings. These are microbenchmarks and loopback ceilings, not production capacity forecasts.

Measured with `go1.26.1` on `linux/arm64` (arm64 (CPU model unavailable)), kernel `Linux 7.0.11-orbstack-00360-gc9bc4d96ac70`, revision `5f5ab63f3fa8`. Build settings: `CGO_ENABLED=1`, `GOARM64=v8.0`, `GODEBUG=(unset)`, `GOEXPERIMENT=(unset)`, `GOFLAGS=(unset)`. Benchmark inputs: `sha256:27890cc60975236d9e5263767b741d2ee125b99f4c1f59d8b8f1a542a8d8d512`. Dependencies: net/http go1.26.1, GoForj Web local checkout, Echo v5.1.0, Gin v1.12.0, Chi v5.3.1, Gorilla Mux v1.8.1, httprouter v1.3.0.

Fiber is omitted because its `fasthttp` engine is not directly comparable in this shared `net/http` suite. See the [benchmark methodology](docs/bench/README.md) and [recorded sample rows](docs/bench/benchmarks_rows.json).

Regenerate the measurement and image with:

```sh
make benchmark-svg
```
<!-- bench:embed:end -->

## API

<!-- api:embed:start -->
Expand Down Expand Up @@ -1265,6 +1287,8 @@ router.GET("/healthz", func(c web.Context) error {
#### <a id="webmiddleware-timeoutwithconfig"></a>webmiddleware.TimeoutWithConfig

TimeoutWithConfig returns a response-timeout middleware with config.
Timed work may run on an isolated native adapter context; request state that
must cross the timeout boundary should use web.Context.Set and web.Context.Get.

```go
router := echoweb.New().Router()
Expand Down
6 changes: 4 additions & 2 deletions adapter/echoweb/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ type Adapter struct {
func New() *Adapter {
engine := echo.New()
engine.IPExtractor = echo.LegacyIPExtractor()
router := &routerAdapter{engine: engine, group: engine}
_, directWebRoutes := engine.Router().(*echo.DefaultRouter)
router := &routerAdapter{engine: engine, group: engine, directWebRoutes: directWebRoutes}
engine.Use(adaptRouterMiddlewares(router))
return &Adapter{
engine: engine,
Expand All @@ -45,7 +46,8 @@ func Wrap(engine *echo.Echo) *Adapter {
if engine.IPExtractor == nil {
engine.IPExtractor = echo.LegacyIPExtractor()
}
router := &routerAdapter{engine: engine, group: engine}
_, directWebRoutes := engine.Router().(*echo.DefaultRouter)
router := &routerAdapter{engine: engine, group: engine, directWebRoutes: directWebRoutes}
engine.Use(adaptRouterMiddlewares(router))
return &Adapter{
engine: engine,
Expand Down
Loading
Loading