📝 Description
During graceful server shutdown or abrupt client disconnections, if an active request's context is canceled while the handler is still executing, the logging middleware (middleware.Logger / middleware.RequestLogger) emits two completion log entries for the same request.
This behavior results in duplicate structured logs, which inflates request metrics (e.g., error rates, throughput) and causes ingestion anomalies in downstream log processing and monitoring systems. The duplication only occurs when the request context is canceled (context.Canceled) before the handler completes its execution lifecycle.
🎯 Acceptance Criteria
🛠️ Technical Specifications & Context
In the go-chi/chi architecture (specifically within the rachelealicek/chi repository), the logging middleware is located in the middleware/ directory:
- Key Files:
middleware/logger.go and potentially middleware/wrap_writer.go.
Root Cause Analysis
The issue typically occurs because:
- The logging middleware defers a write/log operation at the start of the handler chain.
- A separate goroutine or context watcher (e.g., monitoring
r.Context().Done()) detects the cancellation during shutdown and prematurely writes a log entry or response status.
- When the handler chain finally unwinds, the deferred logging function executes anyway, writing a second log entry.
Proposed Solution
To resolve this, we should introduce a state check (e.g., using a thread-safe flag or sync.Once) to ensure the log entry is written exactly once.
-
Introduce a Guard Flag:
In the custom ResponseWriter wrapper or the LogEntry recorder, implement a thread-safe mechanism (such as atomic.CompareAndSwapInt32 or sync.Once) to track whether the log has already been written.
type responseLogger struct {
// ... existing fields
logged int32
}
func (l *responseLogger) WriteLog(status int, bytes int, elapsed time.Duration) {
if atomic.CompareAndSwapInt32(&l.logged, 0, 1) {
// Perform actual logging
}
}
-
Handle Context Cancellation Gracefully:
Ensure that if the context is canceled, the logger captures the cancellation state, marks the log as written, and prevents the deferred execution from writing a duplicate entry.
🧪 Verification & Testing
Automated Test Case
Add a test in middleware/logger_test.go that simulates a canceled request:
- Start a test server using the logging middleware.
- Send a request that blocks inside the handler.
- Cancel the request's context (or close the client connection) to trigger the cancellation path.
- Allow the handler to finish.
- Assert that the custom log buffer/mock logger received exactly one log entry.
Example test structure:
func TestLogger_ContextCancellation_SingleLog(t *testing.T) {
var buf bytes.Buffer
// Initialize logger with buf...
r := chi.NewRouter()
r.Use(middleware.RequestLogger(&middleware.DefaultLogFormatter{Logger: log.New(&buf, "", 0)}))
r.Get("/cancel", func(w http.ResponseWriter, r *http.Request) {
// Simulate work, then context cancellation
ctx, cancel := context.WithCancel(r.Context())
cancel() // Cancel context immediately
<-ctx.Done()
})
// Execute request and verify buf contains exactly one log entry
}
Manual Verification
- Run the test suite with the race detector enabled:
go test -race ./middleware/...

This repo is using Opire - what does it mean? 👇
💵 Everyone can add rewards for this issue commenting /reward 100 (replace 100 with the amount).
🕵️♂️ If someone starts working on this issue to earn the rewards, they can comment /try to let everyone know!
🙌 And when they open the PR, they can comment /claim #1 either in the PR description or in a PR's comment.
🪙 Also, everyone can tip any user commenting /tip 20 @rachelealicek (replace 20 with the amount, and @rachelealicek with the user to tip).
📖 If you want to learn more, check out our documentation.
📝 Description
During graceful server shutdown or abrupt client disconnections, if an active request's context is canceled while the handler is still executing, the logging middleware (
middleware.Logger/middleware.RequestLogger) emits two completion log entries for the same request.This behavior results in duplicate structured logs, which inflates request metrics (e.g., error rates, throughput) and causes ingestion anomalies in downstream log processing and monitoring systems. The duplication only occurs when the request context is canceled (
context.Canceled) before the handler completes its execution lifecycle.🎯 Acceptance Criteria
499 Client Closed Requestor recording the context cancellation error).🛠️ Technical Specifications & Context
In the
go-chi/chiarchitecture (specifically within therachelealicek/chirepository), the logging middleware is located in themiddleware/directory:middleware/logger.goand potentiallymiddleware/wrap_writer.go.Root Cause Analysis
The issue typically occurs because:
r.Context().Done()) detects the cancellation during shutdown and prematurely writes a log entry or response status.Proposed Solution
To resolve this, we should introduce a state check (e.g., using a thread-safe flag or
sync.Once) to ensure the log entry is written exactly once.Introduce a Guard Flag:
In the custom
ResponseWriterwrapper or theLogEntryrecorder, implement a thread-safe mechanism (such asatomic.CompareAndSwapInt32orsync.Once) to track whether the log has already been written.Handle Context Cancellation Gracefully:
Ensure that if the context is canceled, the logger captures the cancellation state, marks the log as written, and prevents the deferred execution from writing a duplicate entry.
🧪 Verification & Testing
Automated Test Case
Add a test in
middleware/logger_test.gothat simulates a canceled request:Example test structure:
Manual Verification
go test -race ./middleware/...This repo is using Opire - what does it mean? 👇
💵 Everyone can add rewards for this issue commenting
/reward 100(replace100with the amount).🕵️♂️ If someone starts working on this issue to earn the rewards, they can comment
/tryto let everyone know!🙌 And when they open the PR, they can comment
/claim #1either in the PR description or in a PR's comment.🪙 Also, everyone can tip any user commenting
/tip 20 @rachelealicek(replace20with the amount, and@rachelealicekwith the user to tip).📖 If you want to learn more, check out our documentation.