Skip to content

fix(echo): use built-in compression middleware instead of manual gzip/deflate#231

Merged
MDA2AV merged 3 commits intoMDA2AV:mainfrom
BennyFranciscus:fix/echo-compression-middleware
Mar 28, 2026
Merged

fix(echo): use built-in compression middleware instead of manual gzip/deflate#231
MDA2AV merged 3 commits intoMDA2AV:mainfrom
BennyFranciscus:fix/echo-compression-middleware

Conversation

@BennyFranciscus
Copy link
Copy Markdown
Collaborator

Replaces hand-rolled gzip/deflate handling with Echo's built-in middleware.GzipWithConfig().

What changed:

  • Removed manual compress/gzip and compress/flate writer setup
  • Use middleware.GzipWithConfig(middleware.GzipConfig{Level: 1}) — same level 1 approach as other framework entries
  • Simplified the compression endpoint to just return the response body, letting the middleware handle encoding

Why:
Same pattern as the Fiber fix (#222 / issue #221). The Echo entry was bypassing the framework's own compression middleware with manual gzip/deflate handling, which isn't how you'd use Echo in production.

Note: Echo's built-in middleware only supports gzip (not deflate). Since gcannon sends Accept-Encoding: gzip, deflate and gzip takes priority, this has no impact on benchmark results.

Fixes #229.

(Reopened from #230 — needed to push go.sum fix from fork due to branch protection)

…zip/deflate

Replace hand-rolled compress/gzip and compress/flate writers in the
/compression endpoint with Echo's built-in middleware.GzipWithConfig()
registered globally at level 1.

This matches the framework-level API rule — Echo provides a documented
gzip middleware, so we use it. The middleware handles Accept-Encoding
negotiation automatically. For non-compression endpoints, the middleware
short-circuits when no Accept-Encoding header is present.

Fixes MDA2AV#229
Echo v4.15.1 middleware imports golang.org/x/time/rate (for rate_limiter.go),
but the go.sum was missing the entry. Run go mod tidy to fix.
@MDA2AV
Copy link
Copy Markdown
Owner

MDA2AV commented Mar 28, 2026

@BennyFranciscus

[test] db endpoint (mixed test prerequisite)
FAIL [GET /db?min=10&max=50]: count=0, rating=False, tags=False, active=False
FAIL [GET /db Content-Type]: expected Content-Type 'application/json', got 'text/html'
FAIL [GET /db empty range]: expected count=0, got -1
[test] static endpoint
FAIL [GET /static/reset.css Content-Type]: expected Content-Type 'text/css', got 'text/html'
FAIL [GET /static/app.js Content-Type]: expected Content-Type 'application/javascript', got 'text/html'
FAIL [GET /static/manifest.json Content-Type]: expected Content-Type 'application/json', got 'text/html'
PASS [static response size] (146 bytes)
PASS [GET /static/nonexistent.txt] (HTTP 404)
[test] async-db endpoint
FAIL [GET /async-db?min=10&max=50]: count=0, rating=False, tags=False, active=False
FAIL [GET /async-db Content-Type]: expected Content-Type 'application/json', got 'text/html'
FAIL [GET /async-db empty range]: expected count=0, got -1

Global gzip middleware was interfering with Content-Type headers
on /db, /static, and /async-db endpoints, causing them to return
text/html instead of application/json and text/css.

Apply gzip as route-level middleware on /compression only.
@BennyFranciscus BennyFranciscus requested a review from MDA2AV as a code owner March 28, 2026 22:16
@BennyFranciscus
Copy link
Copy Markdown
Collaborator Author

Good catch — the global gzip middleware was messing with Content-Type on all routes. Scoped it to just /compression as route-level middleware instead. Should fix the db/static/async-db failures. CI re-running now.

@BennyFranciscus
Copy link
Copy Markdown
Collaborator Author

CI failed but it's a runner network issue — go mod download couldn't reach the Go module proxy via IPv6 (dial tcp [2a00:1450:...]:443: connect: network is unreachable). Not related to the code change. A re-run should fix it.

@MDA2AV
Copy link
Copy Markdown
Owner

MDA2AV commented Mar 28, 2026

/benchmark compression

@github-actions
Copy link
Copy Markdown
Contributor

🚀 Benchmark run triggered for echo (profile: compression). Results will be posted here when done.

@github-actions
Copy link
Copy Markdown
Contributor

Benchmark Results

Framework: echo | Profile: compression

echo / compression / 4096c (p=1, r=0, cpu=unlimited)
  Best: 8781 req/s (CPU: 10741.8%, Mem: 3.8GiB) ===

echo / compression / 16384c (p=1, r=0, cpu=unlimited)
  Best: 8341 req/s (CPU: 11066.8%, Mem: 7.7GiB) ===
Full log
#4 transferring context: 2B done
#4 DONE 0.0s

#5 [internal] load build context
#5 DONE 0.0s

#6 [build 1/6] FROM docker.io/library/golang:1.24-alpine@sha256:8bee1901f1e530bfb4a7850aa7a479d17ae3a18beb6e09064ed54cfd245b7191
#6 resolve docker.io/library/golang:1.24-alpine@sha256:8bee1901f1e530bfb4a7850aa7a479d17ae3a18beb6e09064ed54cfd245b7191 0.1s done
#6 DONE 0.1s

#7 [stage-1 1/2] FROM docker.io/library/alpine:3.19@sha256:6baf43584bcb78f2e5847d1de515f23499913ac9f12bdf834811a3145eb11ca1
#7 resolve docker.io/library/alpine:3.19@sha256:6baf43584bcb78f2e5847d1de515f23499913ac9f12bdf834811a3145eb11ca1 0.1s done
#7 DONE 0.1s

#5 [internal] load build context
#5 transferring context: 18.31kB done
#5 DONE 0.0s

#8 [build 2/6] WORKDIR /app
#8 CACHED

#9 [build 3/6] COPY go.mod go.sum ./
#9 CACHED

#10 [build 4/6] RUN go mod download
#10 CACHED

#11 [build 6/6] RUN CGO_ENABLED=0 go build -o server main.go
#11 CACHED

#12 [build 5/6] COPY main.go ./
#12 CACHED

#13 [stage-1 2/2] COPY --from=build /app/server /server
#13 CACHED

#14 exporting to image
#14 exporting layers done
#14 exporting manifest sha256:1ff2c553b9bfbb87a3fd1ba7fb76006ff7ba7389a90117a4b74a0a99f953bc94 0.0s done
#14 exporting config sha256:33fffc71775c2b89ab597d05b703ee32a4da036e0265c7f7390edcda2ddf7994 done
#14 exporting attestation manifest sha256:e374776ead6257d296f8b8ef27680294e1381c9182a04447731c3613c1647d7c
#14 exporting attestation manifest sha256:e374776ead6257d296f8b8ef27680294e1381c9182a04447731c3613c1647d7c 0.1s done
#14 exporting manifest list sha256:ce328a09ec015123b0a31f7554b8f23406f92f5073ff18e0e640a8d9ea5bc51e 0.0s done
#14 naming to docker.io/library/httparena-echo:latest 0.0s done
#14 unpacking to docker.io/library/httparena-echo:latest
#14 unpacking to docker.io/library/httparena-echo:latest done
#14 DONE 0.3s

==============================================
=== echo / compression / 4096c (p=1, r=0, cpu=unlimited) ===
==============================================
a5a0fddd3db6d1d7aba6c0bfaddc214fceee1caf34f9316e3f910920927bf312
[wait] Waiting for server...
[ready] Server is up

[run 1/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     4096 (64/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   379.80ms   154.00ms   465.50ms    3.72s    4.66s

  41313 requests in 5.00s, 41057 responses
  Throughput: 8.21K req/s
  Bandwidth:  1.84GB/s
  Status codes: 2xx=41057, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 41055 / 41057 responses (100.0%)
  CPU: 10199.0% | Mem: 3.0GiB

[run 2/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     4096 (64/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   440.24ms   410.50ms   639.80ms    2.20s    2.97s

  43592 requests in 5.00s, 43208 responses
  Throughput: 8.64K req/s
  Bandwidth:  1.95GB/s
  Status codes: 2xx=43208, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 43208 / 43208 responses (100.0%)
  CPU: 10814.8% | Mem: 3.6GiB

[run 3/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     4096 (64/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   428.96ms   369.70ms   701.80ms    2.00s    3.06s

  43973 requests in 5.00s, 43905 responses
  Throughput: 8.78K req/s
  Bandwidth:  1.96GB/s
  Status codes: 2xx=43905, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 43905 / 43905 responses (100.0%)
  CPU: 10741.8% | Mem: 3.8GiB

=== Best: 8781 req/s (CPU: 10741.8%, Mem: 3.8GiB) ===
  Input BW: 634.56KB/s (avg template: 74 bytes)
[dry-run] Results not saved (use --save to persist)
httparena-bench-echo
httparena-bench-echo

==============================================
=== echo / compression / 16384c (p=1, r=0, cpu=unlimited) ===
==============================================
9ad7b5da43262ecbda79b2d60d67a662bddc445e786ee17b9950a4a7d9676969
[wait] Waiting for server...
[ready] Server is up

[run 1/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     16384 (256/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   455.97ms   176.50ms   961.30ms    4.24s    4.58s

  53945 requests in 5.00s, 37561 responses
  Throughput: 7.51K req/s
  Bandwidth:  1.70GB/s
  Status codes: 2xx=37561, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 37561 / 37561 responses (100.0%)
  CPU: 9142.9% | Mem: 3.7GiB

[run 2/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     16384 (256/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   905.07ms   714.80ms    1.74s    4.34s    4.84s

  54282 requests in 5.00s, 37898 responses
  Throughput: 7.58K req/s
  Bandwidth:  1.79GB/s
  Status codes: 2xx=37898, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 37898 / 37898 responses (100.0%)
  CPU: 10107.2% | Mem: 7.6GiB

[run 3/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     16384 (256/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   890.59ms   579.40ms    1.99s    4.53s    4.93s

  58093 requests in 5.00s, 41709 responses
  Throughput: 8.34K req/s
  Bandwidth:  1.94GB/s
  Status codes: 2xx=41709, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 41709 / 41709 responses (100.0%)
  CPU: 11066.8% | Mem: 7.7GiB

=== Best: 8341 req/s (CPU: 11066.8%, Mem: 7.7GiB) ===
  Input BW: 602.77KB/s (avg template: 74 bytes)
[dry-run] Results not saved (use --save to persist)
httparena-bench-echo
httparena-bench-echo
[restore] Restoring CPU governor to performance...

@BennyFranciscus
Copy link
Copy Markdown
Collaborator Author

CI passes now ✅ and compression benchmark looks solid — 8.8K req/s at 4096c, 8.3K at 16384c. Consistent with the other Go frameworks doing per-request gzip at level 1. The built-in middleware is doing its job correctly now.

Ready for merge whenever you're happy with it!

@MDA2AV MDA2AV merged commit e9a58e1 into MDA2AV:main Mar 28, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Audit] echo: /compression hand-rolls gzip/deflate instead of using Echo middleware

2 participants