detached c3329b4

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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
8383 requestID := r.Header.Get("X-Request-ID")
8484 if requestID == "" {
8585 requestID = generateRequestID()
86+ // Set on the incoming request so downstream handlers can read it
87+ r.Header.Set("X-Request-ID", requestID)
8688 }
8789 w.Header().Set("X-Request-ID", requestID)
8890
@@ -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 {
363363 }
364364
365365 // 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.
367368 type limitedReadCloser struct {
368369 io.ReadCloser
369370 remaining int64
371+ closed bool
370372 }
371373
372374 func (l *limitedReadCloser) Read(p []byte) (int, error) {
373375 if l.remaining <= 0 {
374376 // 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+ }
376381 return 0, fmt.Errorf("response body exceeded maximum size limit")
377382 }
378383 if int64(len(p)) > l.remaining {
@@ -383,6 +388,14 @@ func (l *limitedReadCloser) Read(p []byte) (int, error) {
383388 return n, err
384389 }
385390
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+
386399 // proxyError writes an error response with security headers appropriate for
387400 // proxy-generated error pages. CSP is set here (not in global middleware)
388401 // 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.ReadCloser369 io.ReadCloser
369 remaining int64370 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/resources376 // 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, err388 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 for399 // 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.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close