fix(proxy) propagate request ID to downstream handlers and prevent double-close in body limiter
Set generated X-Request-ID on the incoming request header so downstream handlers can access it. Guard limitedReadCloser with a closed flag to ensure the underlying body is closed exactly once.
- commit
- c3329b4475d14e252d210b80c754359b81780aaa
- author
- brice · · 2026-02-07 06:00 UTC
- parent
- 441dbcb
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +17 -2
M internal/adapters/in/http/middleware/logging.go +2 -0
| @@ -83,6 +83,8 @@ func RequestLogger(log zerowrap.Logger, trustedNets ...[]*net.IPNet) func(http.H | |||
| 83 | 83 | requestID := r.Header.Get("X-Request-ID") | |
| 84 | 84 | if requestID == "" { | |
| 85 | 85 | requestID = generateRequestID() | |
| 86 | + | // Set on the incoming request so downstream handlers can read it | |
| 87 | + | r.Header.Set("X-Request-ID", requestID) | |
| 86 | 88 | } | |
| 87 | 89 | w.Header().Set("X-Request-ID", requestID) | |
| 88 | 90 | ||
| @@ -83,6 +83,8 @@ func RequestLogger(log zerowrap.Logger, trustedNets ...[]*net.IPNet) func(http.H | |||||
| 83 | requestID := r.Header.Get("X-Request-ID") | 83 | requestID := r.Header.Get("X-Request-ID") | ||
| 84 | if requestID == "" { | 84 | if requestID == "" { | ||
| 85 | requestID = generateRequestID() | 85 | requestID = generateRequestID() | ||
| 86 | + | // Set on the incoming request so downstream handlers can read it | |||
| 87 | + | r.Header.Set("X-Request-ID", requestID) | |||
| 86 | } | 88 | } | ||
| 87 | w.Header().Set("X-Request-ID", requestID) | 89 | w.Header().Set("X-Request-ID", requestID) | ||
| 88 | 90 | ||||
M internal/usecase/proxy/service.go +15 -2
| @@ -363,16 +363,21 @@ func (s *Service) modifyResponse(cfg Config) func(*http.Response) error { | |||
| 363 | 363 | } | |
| 364 | 364 | ||
| 365 | 365 | // limitedReadCloser wraps an io.ReadCloser with a byte limit. | |
| 366 | - | // When the limit is exceeded, subsequent reads return an error. | |
| 366 | + | // When the limit is exceeded, subsequent reads return an error and the | |
| 367 | + | // underlying body is closed exactly once to release upstream resources. | |
| 367 | 368 | type limitedReadCloser struct { | |
| 368 | 369 | io.ReadCloser | |
| 369 | 370 | remaining int64 | |
| 371 | + | closed bool | |
| 370 | 372 | } | |
| 371 | 373 | ||
| 372 | 374 | func (l *limitedReadCloser) Read(p []byte) (int, error) { | |
| 373 | 375 | if l.remaining <= 0 { | |
| 374 | 376 | // Close the underlying body to release upstream connection/resources | |
| 375 | - | l.Close() | |
| 377 | + | if !l.closed { | |
| 378 | + | l.closed = true | |
| 379 | + | l.ReadCloser.Close() | |
| 380 | + | } | |
| 376 | 381 | return 0, fmt.Errorf("response body exceeded maximum size limit") | |
| 377 | 382 | } | |
| 378 | 383 | if int64(len(p)) > l.remaining { | |
| @@ -383,6 +388,14 @@ func (l *limitedReadCloser) Read(p []byte) (int, error) { | |||
| 383 | 388 | return n, err | |
| 384 | 389 | } | |
| 385 | 390 | ||
| 391 | + | func (l *limitedReadCloser) Close() error { | |
| 392 | + | if l.closed { | |
| 393 | + | return nil | |
| 394 | + | } | |
| 395 | + | l.closed = true | |
| 396 | + | return l.ReadCloser.Close() | |
| 397 | + | } | |
| 398 | + | ||
| 386 | 399 | // proxyError writes an error response with security headers appropriate for | |
| 387 | 400 | // proxy-generated error pages. CSP is set here (not in global middleware) | |
| 388 | 401 | // because Gordon proxies arbitrary web apps and a blanket CSP would break them. | |
| @@ -363,16 +363,21 @@ func (s *Service) modifyResponse(cfg Config) func(*http.Response) error { | |||||
| 363 | } | 363 | } | ||
| 364 | 364 | ||||
| 365 | // limitedReadCloser wraps an io.ReadCloser with a byte limit. | 365 | // limitedReadCloser wraps an io.ReadCloser with a byte limit. | ||
| 366 | - | // When the limit is exceeded, subsequent reads return an error. | 366 | + | // When the limit is exceeded, subsequent reads return an error and the |
| 367 | + | // underlying body is closed exactly once to release upstream resources. | |||
| 367 | type limitedReadCloser struct { | 368 | type limitedReadCloser struct { | ||
| 368 | io.ReadCloser | 369 | io.ReadCloser | ||
| 369 | remaining int64 | 370 | remaining int64 | ||
| 371 | + | closed bool | |||
| 370 | } | 372 | } | ||
| 371 | 373 | ||||
| 372 | func (l *limitedReadCloser) Read(p []byte) (int, error) { | 374 | func (l *limitedReadCloser) Read(p []byte) (int, error) { | ||
| 373 | if l.remaining <= 0 { | 375 | if l.remaining <= 0 { | ||
| 374 | // Close the underlying body to release upstream connection/resources | 376 | // Close the underlying body to release upstream connection/resources | ||
| 375 | - | l.Close() | 377 | + | if !l.closed { |
| 378 | + | l.closed = true | |||
| 379 | + | l.ReadCloser.Close() | |||
| 380 | + | } | |||
| 376 | return 0, fmt.Errorf("response body exceeded maximum size limit") | 381 | return 0, fmt.Errorf("response body exceeded maximum size limit") | ||
| 377 | } | 382 | } | ||
| 378 | if int64(len(p)) > l.remaining { | 383 | if int64(len(p)) > l.remaining { | ||
| @@ -383,6 +388,14 @@ func (l *limitedReadCloser) Read(p []byte) (int, error) { | |||||
| 383 | return n, err | 388 | return n, err | ||
| 384 | } | 389 | } | ||
| 385 | 390 | ||||
| 391 | + | func (l *limitedReadCloser) Close() error { | |||
| 392 | + | if l.closed { | |||
| 393 | + | return nil | |||
| 394 | + | } | |||
| 395 | + | l.closed = true | |||
| 396 | + | return l.ReadCloser.Close() | |||
| 397 | + | } | |||
| 398 | + | ||||
| 386 | // proxyError writes an error response with security headers appropriate for | 399 | // proxyError writes an error response with security headers appropriate for | ||
| 387 | // proxy-generated error pages. CSP is set here (not in global middleware) | 400 | // proxy-generated error pages. CSP is set here (not in global middleware) | ||
| 388 | // because Gordon proxies arbitrary web apps and a blanket CSP would break them. | 401 | // because Gordon proxies arbitrary web apps and a blanket CSP would break them. | ||