• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

alexferl / zerohttp / 23176889348

17 Mar 2026 03:22AM UTC coverage: 93.72% (-0.005%) from 93.725%
23176889348

Pull #120

github

web-flow
Merge 73ee91492 into be332c161
Pull Request #120: fix: SSE streaming, metrics defaults, and 404/405 responses

102 of 112 new or added lines in 10 files covered. (91.07%)

12 existing lines in 4 files now uncovered.

9313 of 9937 relevant lines covered (93.72%)

515.68 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.29
/middleware/timeout.go
1
package middleware
2

3
import (
4
        "bytes"
5
        "context"
6
        "errors"
7
        "net/http"
8
        "sync"
9

10
        "github.com/alexferl/zerohttp/config"
11
        zconfig "github.com/alexferl/zerohttp/internal/config"
12
        "github.com/alexferl/zerohttp/internal/problem"
13
        "github.com/alexferl/zerohttp/metrics"
14
)
15

16
// ErrTimeoutWrite is returned when the timeout middleware fails to write response data.
17
var ErrTimeoutWrite = errors.New("zerohttp: timeout middleware write failed")
18

19
// Timeout is a middleware that enforces request timeouts by canceling the context
20
// after a specified duration. When the timeout is exceeded, it returns an HTTP 504
21
// Gateway Timeout response to the client.
22
//
23
// Important: Your handler must monitor the ctx.Done() channel to detect when the
24
// context deadline has been reached. If you don't check this channel and return
25
// appropriately, the timeout mechanism will be ineffective and the request will
26
// continue processing beyond the intended timeout period.
27
func Timeout(cfg ...config.TimeoutConfig) func(http.Handler) http.Handler {
32✔
28
        c := config.DefaultTimeoutConfig
32✔
29
        if len(cfg) > 0 {
61✔
30
                zconfig.Merge(&c, cfg[0])
29✔
31
        }
29✔
32

33
        return func(next http.Handler) http.Handler {
64✔
34
                return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
70✔
35
                        for _, exemptPath := range c.ExemptPaths {
39✔
36
                                if pathMatches(r.URL.Path, exemptPath) {
2✔
37
                                        next.ServeHTTP(w, r)
1✔
38
                                        return
1✔
39
                                }
1✔
40
                        }
41

42
                        ctx, cancel := context.WithTimeout(r.Context(), c.Timeout)
37✔
43
                        defer cancel()
37✔
44

37✔
45
                        done := make(chan struct{})
37✔
46
                        panicChan := make(chan any, 1)
37✔
47

37✔
48
                        tw := &timeoutWriter{
37✔
49
                                w:   w,
37✔
50
                                h:   make(http.Header),
37✔
51
                                req: r,
37✔
52
                        }
37✔
53

37✔
54
                        go func() {
74✔
55
                                defer func() {
74✔
56
                                        if p := recover(); p != nil {
38✔
57
                                                panicChan <- p
1✔
58
                                        }
1✔
59
                                }()
60
                                next.ServeHTTP(tw, r.WithContext(ctx))
37✔
61
                                close(done)
37✔
62
                        }()
63

64
                        select {
37✔
65
                        case p := <-panicChan:
1✔
66
                                panic(p)
1✔
67
                        case <-done:
24✔
68
                                tw.mu.Lock()
24✔
69
                                defer tw.mu.Unlock()
24✔
70

24✔
71
                                dst := w.Header()
24✔
72
                                for k, v := range tw.h {
25✔
73
                                        dst[k] = v
1✔
74
                                }
1✔
75

76
                                if !tw.wroteHeader {
24✔
77
                                        tw.code = http.StatusOK
×
78
                                }
×
79
                                w.WriteHeader(tw.code)
24✔
80
                                _, _ = w.Write(tw.wbuf.Bytes()) // Best effort write
24✔
81
                        case <-ctx.Done():
12✔
82
                                tw.mu.Lock()
12✔
83
                                defer tw.mu.Unlock()
12✔
84

12✔
85
                                if errors.Is(ctx.Err(), context.DeadlineExceeded) {
24✔
86
                                        metrics.SafeRegistry(metrics.GetRegistry(r.Context())).Counter("timeout_requests_total").Inc()
12✔
87

12✔
88
                                        detail := problem.NewDetail(c.StatusCode, c.Message)
12✔
89
                                        _ = detail.RenderAuto(w, r) // Best effort - client may have disconnected
12✔
90
                                        tw.err = ErrTimeoutWrite
12✔
91
                                }
12✔
92
                        }
93
                })
94
        }
95
}
96

97
type timeoutWriter struct {
98
        w    http.ResponseWriter
99
        h    http.Header
100
        wbuf bytes.Buffer
101
        req  *http.Request
102

103
        mu          sync.Mutex
104
        err         error
105
        wroteHeader bool
106
        code        int
107
}
108

109
func (tw *timeoutWriter) Header() http.Header {
1✔
110
        return tw.h
1✔
111
}
1✔
112

113
func (tw *timeoutWriter) Write(p []byte) (int, error) {
25✔
114
        tw.mu.Lock()
25✔
115
        defer tw.mu.Unlock()
25✔
116

25✔
117
        if tw.err != nil {
25✔
118
                return 0, tw.err
×
119
        }
×
120

121
        if !tw.wroteHeader {
48✔
122
                tw.writeHeaderLocked(http.StatusOK)
23✔
123
        }
23✔
124

125
        return tw.wbuf.Write(p)
25✔
126
}
127

128
func (tw *timeoutWriter) WriteHeader(code int) {
2✔
129
        tw.mu.Lock()
2✔
130
        defer tw.mu.Unlock()
2✔
131
        tw.writeHeaderLocked(code)
2✔
132
}
2✔
133

134
func (tw *timeoutWriter) writeHeaderLocked(code int) {
27✔
135
        if tw.err != nil {
27✔
136
                return
×
137
        }
×
138

139
        if tw.wroteHeader {
27✔
140
                return
×
141
        }
×
142

143
        tw.wroteHeader = true
27✔
144
        tw.code = code
27✔
145
}
146

147
// Flush implements http.Flusher. Flushes buffered content to the underlying writer.
148
func (tw *timeoutWriter) Flush() {
3✔
149
        tw.mu.Lock()
3✔
150
        defer tw.mu.Unlock()
3✔
151

3✔
152
        if tw.err != nil {
3✔
NEW
153
                return
×
NEW
154
        }
×
155

156
        if !tw.wroteHeader {
5✔
157
                tw.writeHeaderLocked(http.StatusOK)
2✔
158
        }
2✔
159

160
        // Flush buffered content to underlying writer
161
        if tw.wbuf.Len() > 0 {
4✔
162
                _, _ = tw.w.Write(tw.wbuf.Bytes())
1✔
163
                tw.wbuf.Reset()
1✔
164
        }
1✔
165

166
        if f, ok := tw.w.(http.Flusher); ok {
6✔
167
                f.Flush()
3✔
168
        }
3✔
169
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc