Skip to content

feat(gateway): gateway improvements roadmap items#596

Open
poyrazK wants to merge 50 commits into
mainfrom
release/gateway-features
Open

feat(gateway): gateway improvements roadmap items#596
poyrazK wants to merge 50 commits into
mainfrom
release/gateway-features

Conversation

@poyrazK
Copy link
Copy Markdown
Owner

@poyrazK poyrazK commented May 18, 2026

Summary

PR #596 adds comprehensive gateway improvements: JWT authentication with JWKS, mTLS support, per-route CORS, gzip compression, circuit breaker with retry, per-route rate limiting, and observability metrics. All features are documented with ADRs.

Changes

New Features

  • JWT Authentication: JWKS-backed validation with issuer/audience verification, RSA key parsing from JWK n/e parameters, singleflight deduplication, circuit breaker (3 failures, 30s reset), 5-minute cache TTL, claims propagated to upstream via X-JWT-Claim-* headers
  • mTLS Support: Configurable client certificates (PEM) and CA certificates for backend TLS verification with descriptive errors on malformed certs
  • Per-route CORS: allowed_origins, allowed_methods, allowed_headers, expose_headers, max_age; Access-Control-Allow-Credentials set appropriately
  • Gzip Compression: Response compression when client advertises support, gzip writer flushed after proxy.ServeHTTP
  • Dry-run Validation: Route creation with ?dry_run=true validates CIDR blocks, TLS conflicts, mTLS pairing without persisting
  • Circuit Breaker + Retry: Per-route CB with configurable threshold/timeout; automatic retry with exponential backoff (100ms base, 2x multiplier, capped at retry_timeout) for idempotent methods (GET, HEAD, PUT, DELETE, OPTIONS) on 502/503/504/429
  • Per-route Rate Limiting: Token-bucket algorithm with per-client tracking (API key prefix or IP), burst = rateLimit * 2
  • Response Header Stripping: strip_response_headers field removes configured headers from upstream responses

Observability

  • thecloud_gateway_upstream_latency_seconds (histogram, labels: route_id, method, path)
  • thecloud_gateway_retry_total (counter, labels: route_id, status: retry/exhausted)
  • thecloud_gateway_circuit_breaker_state (gauge, labels: route_id, 0=closed/1=open/2=half-open)
  • thecloud_jwks_fetch_total (counter, labels: status: success/error/circuit_open)
  • thecloud_jwks_breaker_state (gauge)
  • W3C TraceContext (traceparent, tracestate) preserved or generated

Files Changed

File Changes
internal/core/domain/gateway.go Add JWT, mTLS, CORS, compression, circuit breaker, retry fields
internal/core/ports/gateway.go Add new fields to CreateRouteParams
internal/core/services/gateway.go JWT validation, JWKS cache, circuit breaker, retry transport, mTLS config
internal/handlers/gateway_handler.go CORS injection, compression, trace headers, JWT validation in Proxy
internal/api/setup/router.go Router setup for new features
internal/platform/metrics.go JWKS metrics, gateway metrics
internal/handlers/gateway_handler_test.go JWT tests, CORS tests, compression tests, trace tests
internal/core/services/gateway_test.go GetProxy return count fix

Documentation

  • docs/FEATURES.md — updated API Gateway section
  • docs/services/cloud-gateway.md — expanded with full route config table, JWT flow, metrics
  • docs/adr/ADR-028-gateway-per-route-rate-limiting.md — new ADR
  • docs/adr/ADR-029-gateway-security-resilience.md — new ADR

Test Plan

  • Unit tests added/updated (go test ./internal/handlers/... -run Gateway -v)

  • Integration tests pass (GetProxy 3→4 return count fix)

  • Manual testing performed — JWT, CORS, compression, dry-run verified

  • Code follows project style guidelines

  • Documentation updated (FEATURES.md, cloud-gateway.md, ADR-028, ADR-029)

  • Tests added/updated (5 new handler tests for JWT, CORS, compression, trace)

  • All tests pass (go build ./... clean)

Breaking Changes

None — all new fields are optional with omitempty. Existing routes continue to work without changes.

Related Issues

Related to: api-gateway-improvement-roadmap

Summary by CodeRabbit

  • New Features

    • Gateway: per-route JWKS-backed JWT auth with claim propagation, per-route mTLS, CORS, gzip compression, per-route circuit breaker with retries/backoff, enhanced per-route rate limiting, response-header stripping, and route dry-run validation
  • Breaking Changes

    • Several API routes removed or relocated; IAM policy route registration reordered
  • Documentation

    • Added ADRs, roadmap, and expanded gateway docs; updated OpenAPI/Swagger schemas
  • Observability

    • New Prometheus metrics for upstream latency, retries, and circuit breaker state

Review Change Stack

Copilot AI review requested due to automatic review settings May 18, 2026 21:19
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

Warning

Review limit reached

@poyrazK, we couldn't start this review because you've used your available PR reviews for now.

Your plan currently allows 1 review/hour. Refill in 12 minutes and 23 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a2c358fb-8a51-492c-9619-bc360fe8c07b

📥 Commits

Reviewing files that changed from the base of the PR and between 9de3441 and d97bb53.

📒 Files selected for processing (1)
  • internal/core/services/gateway.go
📝 Walkthrough

Walkthrough

Adds per-route gateway features: JWKS-backed JWT validation, per-route circuit-breaker and retry transport, per-route rate limiting, CORS, mTLS/backend CA support, compression, response header stripping, expanded route contracts and schemas, router two-phase handler init, metrics, docs, and extensive handler tests.

Changes

Gateway Feature Extensions

Layer / File(s) Summary
Contracts & API schemas
internal/core/domain/gateway.go, internal/core/ports/gateway.go, go.mod, docs/swagger/*, docs/FEATURES.md
Expanded GatewayRoute and CreateRouteParams with CORS, compression, JWT/JWKS, mTLS/CA, and response header stripping fields; GatewayService adds ValidateJWT. Promoted golang.org/x/sync v0.19.0 to a direct dependency and updated generated Swagger/OpenAPI schemas and FEATURES doc.
Service: JWKS, TLS, and Resilient Transport
internal/core/services/gateway.go
Implements JWKS fetch/cache with singleflight and circuit breaker, ValidateJWT parsing/verifying RSA-signed tokens and audience/issuer checks, buildTLSConfig with optional mTLS and backend CA, and retryTransport with per-route circuit breaker, retry/backoff/jitter, retry metrics, response draining, and improved retryable error detection.
Gateway Handler & Tests
internal/handlers/gateway_handler.go, internal/handlers/gateway_handler_test.go
GatewayHandler receives a rate limiter; CreateRoute supports dry-run and mTLS pairing validation; Proxy enforces optional JWT (Bearer) via ValidateJWT, applies per-route rate limiting (API-key or client IP), enforces request size limits, optionally compresses responses, strips configured response headers, injects CORS headers, and records upstream latency. Tests cover JWT auth, CORS (including preflight), compression (including gzip flush), trace preservation, and dry-run validation.
Router Initialization, Metrics & Docs
internal/api/setup/router.go, internal/platform/metrics.go, docs/adr/*, docs/services/*, docs/roadmaps/*
Defers Gateway handler initialization until the global IP rate limiter exists (two-phase init). Adds Prometheus metrics: GatewayUpstreamLatency, GatewayRetryTotal, GatewayCircuitBreakerState. Adds ADR-028 (per-route rate limiting) and ADR-029 (security/resilience), updates service docs and roadmap, and removes several route registrations.
Core Service Tests & Circuit Breaker
internal/core/services/gateway_test.go, internal/core/services/gateway_retry_test.go, internal/platform/circuit_breaker.go
Small test updates to capture paramsMap from GetProxy returns and to assert errors on exhausted retries; circuit breaker exposes RecordFailure() to synchronously record failures and transition state.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Handler as GatewayHandler
  participant RateLimiter
  participant JWTService as GatewayService
  participant ReverseProxy
  participant Backend
  Client->>Handler: HTTP request + headers
  Handler->>Handler: Match route config
  alt Route has JWKS
    Handler->>JWTService: ValidateJWT(ctx, route, token)
    JWTService->>JWTService: Check cache/singleflight + circuit breaker
    JWTService->>JWTService: Parse and verify token
    JWTService-->>Handler: Claims map
    Handler->>Handler: Inject X-JWT-Claim-* headers
  end
  Handler->>RateLimiter: GetRouteLimiter(route.ID, key, rate, burst)
  alt Rate limited
    Handler-->>Client: 429 Too Many Requests
  else Allowed
    Handler->>ReverseProxy: Forward (with TLS/mTLS)
    ReverseProxy->>Backend: RoundTrip (retryTransport handles retries/CB)
    Backend-->>ReverseProxy: Response
    Handler->>Handler: Apply CORS, strip headers, compress if configured
    Handler-->>Client: Final response
  end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

  • poyrazK/thecloud#378: Shares per-route limiter storage and GetRouteLimiter integration used by the handler.
  • poyrazK/thecloud#395: Overlaps in per-route circuit-breaker and retry transport design and config fields.
  • poyrazK/thecloud#154: Related changes touching the circuit breaker primitive that may overlap with RecordFailure additions.

Poem

🐰 The gateway springs, a careful hop,

JWKS and limits guard each stop,
Retries and CORS and headers pruned,
mTLS and metrics hum in tune,
A rabbit cheers this routed bloom.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(gateway): gateway improvements roadmap items' is related to the changeset but is overly broad and does not highlight the most important specific changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch release/gateway-features

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

poyrazK added 2 commits May 19, 2026 12:10
- Fix NewGatewayHandler signature (3 args: svc, rateLimiter, logger)
- Add ValidateJWT to mockGatewayService to satisfy updated interface
- Add TestGatewayHandlerInjectCORSHeaders (wildcard, exact match, deny, methods/headers/maxAge)
- Add TestGatewayHandlerProxyCompression (gzip enabled, disabled, client-no-accept)
- Add TestGatewayHandlerCreateRouteDryRun (valid CIDR, invalid CIDR)
- Add strings and net imports to test file
JWT validation:
- Implement RSA key parsing from JWKS (parseRSAPublicKeyFromJWK)
- Store JWKS keys as *rsa.PublicKey, not map[string]any
- Add signing method check (RSA only)
- Fix empty token bypass — return 401 for missing/invalid auth header
- Add audience verification supporting []interface{} (RFC 7519)

JWKS fetch:
- Add 10s timeout to httpClient

mTLS:
- buildTLSConfig returns error on bad cert/key/CA instead of silently continuing

Retry transport:
- Fix jitter() — use rand.Float64() instead of broken fraction
- Drain response body on retry exhaustion

Compression:
- Call compressWriter.Close() to flush gzip

CORS:
- Add Access-Control-Allow-Credentials when origin is not wildcard

Tests:
- Add TestGatewayHandlerProxyJWTEmptyToken
- Add TestGatewayHandlerProxyJWTMissingBearer
Copilot AI review requested due to automatic review settings May 19, 2026 09:35
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

poyrazK added 2 commits May 19, 2026 12:42
- parseRSAPublicKeyFromJWK: add exponent overflow bounds check (e >= 1<<30)
- verifyAudience: already a package-level function (no change needed)
- TestGatewayHandlerInjectCORSHeadersPreflight: add OPTIONS test with CORS headers
Copilot AI review requested due to automatic review settings May 19, 2026 09:45
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

- Add jwksCircuitBreaker to GatewayService (3 failures, 30s reset)
- Wrap JWKS HTTP fetch in circuit breaker Execute()
- Return error when JWKS has keys but none parse as valid RSA
  (instead of caching empty map and failing all future validations)
poyrazK added 2 commits May 22, 2026 16:00
Close gzip writer before returning to pool to prevent 'read on closed
response body' errors when reverse proxy tries to copy response data.
Copilot AI review requested due to automatic review settings May 22, 2026 13:24
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

poyrazK added 2 commits May 22, 2026 16:38
Add safe type assertion with fallback error for Hijack() method to
prevent panic when embedded ResponseWriter doesn't implement Hijacker.
Temporarily disable compression to isolate E2E test failures. The sync.Pool
gzip implementation appears to cause 'read on closed response body' errors.
Compression can be re-enabled once the underlying issue is identified.
Copilot AI review requested due to automatic review settings May 24, 2026 11:31
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

poyrazK added 2 commits May 24, 2026 14:47
Use Flush() instead of Close() to flush buffered data to the underlying
response without closing it. The underlying writer should remain open for
ReverseProxy to use.
The sync.Pool-based gzip implementation causes 'read on closed response
body' errors in E2E tests. This reverts to creating a new gzip.Writer
per request, trading some performance for correctness.
Copilot AI review requested due to automatic review settings May 24, 2026 11:57
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

poyrazK added 2 commits May 24, 2026 15:10
Compress gzip entirely to determine if the 'read on closed response body'
errors are caused by compression or something else in the proxy path.
Re-enable gzip compression after debugging. The implementation now creates
a new gzip.Writer per request (no sync.Pool) and uses Flush() in Close()
to ensure data is written without closing the underlying writer.
Copilot AI review requested due to automatic review settings May 24, 2026 12:26
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
internal/core/services/gateway.go (1)

786-802: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replay request bodies for retryable HTTP statuses and don’t return responses with closed bodies

  • In retryTransport.doRoundTrip, the retryable-status success path drains/closes resp.Body and then immediately continues (lines 798-802), which skips the req.GetBody() replay block (lines 831-839). Retries for idempotent methods with bodies can therefore reuse an already-consumed request stream.
  • resp.Body is also drained and closed before returning resp on non-retryable statuses (lines 790-797), so callers can receive a closed resp.Body. The same can happen for exhausted retryable statuses because RoundTrip returns se.resp (lines 752-755) after lastResp was already drained/closed (lines 798-802, 845-848).
Suggested fix
 		if err == nil {
 			// Reset consecutive error counter on success
 			rt.consecutiveConnErrors.Store(0)
 			if !rt.isRetryableStatus(resp.StatusCode) {
 				// Drain body before returning so connection can be reused
 				if resp.Body != nil {
 					_, _ = io.Copy(io.Discard, resp.Body)
 					resp.Body.Close()
 				}
 				return resp, nil //nolint:bodyclose
 			}
 			// drain and close body so connection can be reused, then retry
 			_, _ = io.Copy(io.Discard, resp.Body)
 			_ = resp.Body.Close()
 			lastResp = resp
+			if attempt < maxAttempts-1 && req.GetBody != nil {
+				body, getBodyErr := req.GetBody()
+				if getBodyErr != nil {
+					return nil, getBodyErr
+				}
+				req = req.Clone(req.Context())
+				req.Body = body
+			}
 			continue
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/core/services/gateway.go` around lines 786 - 802, The retry logic in
retryTransport.doRoundTrip improperly drains/closes resp.Body before either
retrying or returning and also skips rebuilding the request body with
req.GetBody() when continuing; change the flow so that on non-retryable statuses
you do not drain/close resp.Body before returning (leave body open to caller),
and on retryable statuses capture lastResp without closing its Body, then before
each retry attempt call req.GetBody() to recreate and set req.Body (and handle
nil/GetBody errors) so the request stream is replayed correctly; update usage
around rt.base.RoundTrip, rt.isRetryableStatus, req.GetBody, lastResp and
rt.consecutiveConnErrors to ensure bodies are only drained/closed by the final
returner, not during intermediate retry bookkeeping.
internal/handlers/gateway_handler.go (1)

375-417: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix streaming by preserving http.Flusher, and don’t swallow gzip close errors in compressWriter.

  • compressWriter (in internal/handlers/gateway_handler.go) forwards Hijack but not http.Flusher, so proxy.ServeHTTP can’t flush incremental output for streaming/SSE when compression is enabled.
  • compressWriter.Close() discards gzip.Writer.Close()’s error (_ = cw.gz.Close()), and GatewayHandler.Proxy ignores cw.Close()’s returned error; return/handle these errors instead of silently dropping them.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/handlers/gateway_handler.go` around lines 375 - 417, compressWriter
currently forwards Hijack but not http.Flusher and it swallows gzip close
errors; implement Flush on compressWriter to forward to the underlying
http.Flusher (and call cw.gz.Flush() if gz != nil) so streaming/SSE works when
compression is enabled, ensure WriteHeader/Write still set the status field
correctly, change Close to return the gzip.Writer.Close() error instead of
discarding it, and update GatewayHandler.Proxy (where compressWriter is used) to
call cw.Close() and handle or log its returned error instead of ignoring it.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/handlers/gateway_handler.go`:
- Around line 403-408: compressWriter.Close currently swallows errors by
assigning `_ = cw.gz.Close()` and always returning nil; change
compressWriter.Close to return the error from cw.gz.Close() (and still set cw.gz
= nil) so gzip trailer write failures are propagated, then update
GatewayHandler.Proxy to check and handle the error returned from cw.Close()
(e.g., call httputil.Error(c, err) or otherwise surface the error) instead of
ignoring it; target the compressWriter.Close method and the GatewayHandler.Proxy
code path that calls cw.Close().

---

Outside diff comments:
In `@internal/core/services/gateway.go`:
- Around line 786-802: The retry logic in retryTransport.doRoundTrip improperly
drains/closes resp.Body before either retrying or returning and also skips
rebuilding the request body with req.GetBody() when continuing; change the flow
so that on non-retryable statuses you do not drain/close resp.Body before
returning (leave body open to caller), and on retryable statuses capture
lastResp without closing its Body, then before each retry attempt call
req.GetBody() to recreate and set req.Body (and handle nil/GetBody errors) so
the request stream is replayed correctly; update usage around rt.base.RoundTrip,
rt.isRetryableStatus, req.GetBody, lastResp and rt.consecutiveConnErrors to
ensure bodies are only drained/closed by the final returner, not during
intermediate retry bookkeeping.

In `@internal/handlers/gateway_handler.go`:
- Around line 375-417: compressWriter currently forwards Hijack but not
http.Flusher and it swallows gzip close errors; implement Flush on
compressWriter to forward to the underlying http.Flusher (and call cw.gz.Flush()
if gz != nil) so streaming/SSE works when compression is enabled, ensure
WriteHeader/Write still set the status field correctly, change Close to return
the gzip.Writer.Close() error instead of discarding it, and update
GatewayHandler.Proxy (where compressWriter is used) to call cw.Close() and
handle or log its returned error instead of ignoring it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6294bc0b-77b7-45cb-a5bd-b9b172e00dd0

📥 Commits

Reviewing files that changed from the base of the PR and between d6461e2 and 9de3441.

📒 Files selected for processing (3)
  • internal/core/services/gateway.go
  • internal/handlers/gateway_handler.go
  • internal/platform/metrics.go

Comment on lines +403 to +408
func (cw *compressWriter) Close() error {
if cw.gz != nil {
_ = cw.gz.Close()
cw.gz = nil
}
return nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the file and show around the specified lines
sed -n '360,460p' internal/handlers/gateway_handler.go

Repository: poyrazK/thecloud

Length of output: 3006


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find compressWriter usage and Close() calls
rg -n "newCompressWriter|compressWriter|\.Close\(\)" internal/handlers/gateway_handler.go

# 2) Also search whole internal/handlers for compressWriter.Close usage
rg -n "compressWriter\)\.Close\(\)|newCompressWriter\(|type compressWriter" internal/handlers

# 3) Show surrounding code where Close() is likely called
# (limit output by targeting exact file/line ranges)
sed -n '250,420p' internal/handlers/gateway_handler.go
sed -n '420,560p' internal/handlers/gateway_handler.go

Repository: poyrazK/thecloud

Length of output: 9578


Return gzip trailer write errors instead of discarding them in compressWriter.Close().

compressWriter.Close() currently does _ = cw.gz.Close() and always returns nil; GatewayHandler.Proxy also ignores the returned error. This drops gzip trailer write failures and violates the “no silent failures” guideline.

File: internal/handlers/gateway_handler.go
Lines: 403-408

func (cw *compressWriter) Close() error {
	if cw.gz != nil {
		_ = cw.gz.Close()
		cw.gz = nil
	}
	return nil
}
Suggested fix
 func (cw *compressWriter) Close() error {
 	if cw.gz != nil {
-		_ = cw.gz.Close()
+		if err := cw.gz.Close(); err != nil {
+			return err
+		}
 		cw.gz = nil
 	}
 	return nil
 }

As per coding guidelines, "Do not use silent failures - avoid blank identifier assignment like _ = someFunc()". Also handle the returned error from cw.Close() in GatewayHandler.Proxy (e.g., via httputil.Error(c, err)).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (cw *compressWriter) Close() error {
if cw.gz != nil {
_ = cw.gz.Close()
cw.gz = nil
}
return nil
func (cw *compressWriter) Close() error {
if cw.gz != nil {
if err := cw.gz.Close(); err != nil {
return err
}
cw.gz = nil
}
return nil
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/handlers/gateway_handler.go` around lines 403 - 408,
compressWriter.Close currently swallows errors by assigning `_ = cw.gz.Close()`
and always returning nil; change compressWriter.Close to return the error from
cw.gz.Close() (and still set cw.gz = nil) so gzip trailer write failures are
propagated, then update GatewayHandler.Proxy to check and handle the error
returned from cw.Close() (e.g., call httputil.Error(c, err) or otherwise surface
the error) instead of ignoring it; target the compressWriter.Close method and
the GatewayHandler.Proxy code path that calls cw.Close().

poyrazK added 2 commits May 24, 2026 15:42
When a retryable status (502, 503, 504, 429) is received and retries
are exhausted, the retry transport was draining AND closing the body
before returning the response to the caller. This caused the reverse
proxy's copy logic to fail with "read on closed response body" since
the caller (httputil.ReverseProxy) still needed to read the body to
forward it to the client.

Now we drain without closing, preserving the caller's ability to read
and forward the response body. The body remains valid until the caller
consumes or closes it.
Copilot AI review requested due to automatic review settings May 24, 2026 13:03
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

…ponses

The ReverseProxy needs to read from the response body to forward to the
client. Draining and closing it before returning to the caller caused
"read on closed response body" errors. Now we pass the response through
without touching the body — the Transport handles connection reuse.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants