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

alexferl / zerohttp / 23093061092

14 Mar 2026 05:45PM UTC coverage: 93.164% (+0.5%) from 92.697%
23093061092

push

github

web-flow
feat: add AcceptsJSON and RenderAuto for content negotiation (#105)

* feat: add AcceptsJSON and RenderAuto for content negotiation

- Add AcceptsJSON function to detect JSON-capable clients
- Add RenderAuto method that returns JSON or plain text based on Accept header
- Add tests for both AcceptsJSON and RenderAuto
- Update middleware to use RenderAuto instead of Render

Signed-off-by: alexferl <me@alexferl.com>

* increase coverage

Signed-off-by: alexferl <me@alexferl.com>

---------

Signed-off-by: alexferl <me@alexferl.com>

64 of 67 new or added lines in 17 files covered. (95.52%)

7 existing lines in 2 files now uncovered.

8899 of 9552 relevant lines covered (93.16%)

76.67 hits per line

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

87.63
/server_tls.go
1
package zerohttp
2

3
import (
4
        "crypto/tls"
5
        "fmt"
6
        "net"
7
        "net/http"
8
        "sync"
9
        "time"
10

11
        "github.com/alexferl/zerohttp/config"
12
        "github.com/alexferl/zerohttp/log"
13
)
14

15
// ListenAndServeTLS starts the HTTPS server with the specified certificate files.
16
// It creates a TLS listener if one is not already configured and serves HTTPS
17
// traffic using the provided certificate and key files. If the TLS server is
18
// not configured, this method logs a debug message and returns nil without error.
19
//
20
// Parameters:
21
//   - certFile: Path to the TLS certificate file in PEM format
22
//   - keyFile: Path to the TLS private key file in PEM format
23
//
24
// This method blocks until the server encounters an error or is shut down.
25
// Returns any error encountered while starting or running the TLS server.
26
func (s *Server) ListenAndServeTLS(certFile, keyFile string) error {
6✔
27
        s.mu.Lock()
6✔
28

6✔
29
        if s.tlsServer == nil {
6✔
30
                s.mu.Unlock()
×
31
                s.logger.Debug("TLS server not configured, skipping")
×
32
                return nil
×
33
        }
×
34

35
        s.logger.Debug("TLS server is configured, proceeding")
6✔
36

6✔
37
        // Load certificates if provided
6✔
38
        if certFile != "" && keyFile != "" {
12✔
39
                s.logger.Debug("Loading TLS certificates", log.F("cert", certFile), log.F("key", keyFile))
6✔
40
                cert, err := tls.LoadX509KeyPair(certFile, keyFile)
6✔
41
                if err != nil {
9✔
42
                        s.mu.Unlock()
3✔
43
                        s.logger.Error("Failed to load TLS certificates", log.E(err))
3✔
44
                        return fmt.Errorf("failed to load certificates: %w", err)
3✔
45
                }
3✔
46
                if s.tlsServer.TLSConfig == nil {
6✔
47
                        s.tlsServer.TLSConfig = &tls.Config{}
3✔
48
                }
3✔
49
                s.tlsServer.TLSConfig.Certificates = []tls.Certificate{cert}
3✔
50
        }
51

52
        var err error
3✔
53
        if s.tlsListener == nil {
6✔
54
                s.logger.Debug("Creating TLS listener", log.F("addr", s.tlsServer.Addr))
3✔
55
                s.tlsListener, err = tls.Listen("tcp", s.tlsServer.Addr, s.tlsServer.TLSConfig)
3✔
56
                if err != nil {
3✔
57
                        s.logger.Error("Failed to create TLS listener", log.E(err))
×
58
                        s.mu.Unlock()
×
59
                        return err
×
60
                }
×
61
                s.logger.Debug("TLS listener created successfully")
3✔
62
        }
63

64
        s.mu.Unlock()
3✔
65

3✔
66
        s.logger.Info("Starting HTTPS server",
3✔
67
                log.F("addr", fmtHTTPSAddr(s.tlsListener.Addr().String())),
3✔
68
                log.F("cert_file", certFile),
3✔
69
                log.F("key_file", keyFile))
3✔
70

3✔
71
        // Start HTTP/3 server in background if configured
3✔
72
        if s.http3Server != nil {
5✔
73
                go func() {
4✔
74
                        s.logger.Info("Starting HTTP/3 server",
2✔
75
                                log.F("cert_file", certFile),
2✔
76
                                log.F("key_file", keyFile))
2✔
77
                        if err := s.http3Server.ListenAndServeTLS(certFile, keyFile); err != nil {
3✔
78
                                s.logger.Error("HTTP/3 server error", log.E(err))
1✔
79
                        }
1✔
80
                }()
81
        }
82

83
        // Start WebTransport server in background if configured
84
        if s.webTransportServer != nil {
4✔
85
                go func() {
2✔
86
                        s.logger.Info("Starting WebTransport server",
1✔
87
                                log.F("cert_file", certFile),
1✔
88
                                log.F("key_file", keyFile))
1✔
89
                        if err := s.webTransportServer.ListenAndServeTLS(certFile, keyFile); err != nil {
1✔
90
                                s.logger.Error("WebTransport server error", log.E(err))
×
91
                        }
×
92
                }()
93
        }
94

95
        // Use Serve (not ServeTLS) since we already have a tls.Listener
96
        return s.tlsServer.Serve(s.tlsListener)
3✔
97
}
98

99
// StartTLS is a convenience method that starts only the HTTPS server with
100
// the specified certificate files. If the TLS server is not configured,
101
// this method returns nil without error.
102
//
103
// Parameters:
104
//   - certFile: Path to the TLS certificate file in PEM format
105
//   - keyFile: Path to the TLS private key file in PEM format
106
//
107
// This is equivalent to calling ListenAndServeTLS directly.
108
// Returns any error encountered while starting or running the TLS server.
109
func (s *Server) StartTLS(certFile, keyFile string) error {
2✔
110
        if s.tlsServer == nil {
3✔
111
                return fmt.Errorf("TLS server not configured")
1✔
112
        }
1✔
113

114
        return s.ListenAndServeTLS(certFile, keyFile)
1✔
115
}
116

117
// StartAutoTLS starts the server with automatic TLS certificate management using Let's Encrypt.
118
// It starts both HTTP (for ACME challenges) and HTTPS servers.
119
// The HTTP server redirects to HTTPS and handles ACME challenges.
120
//
121
// Users must configure the AutocertManager with their desired host policy before calling
122
// this method. For example, using golang.org/x/crypto/acme/autocert:
123
//
124
//        mgr := &autocert.Manager{
125
//            Cache:      autocert.DirCache("/var/cache/certs"),
126
//            Prompt:     autocert.AcceptTOS,
127
//            HostPolicy: autocert.HostWhitelist("example.com"),
128
//        }
129
//        srv := zerohttp.New(config.WithAutocertManager(mgr))
130
//        srv.StartAutoTLS()
131
//
132
// The HTTP server handles:
133
//   - ACME challenge requests from Let's Encrypt
134
//   - Redirects all other HTTP traffic to HTTPS
135
//
136
// Returns an error if the autocert manager is not configured or if any server fails to start.
137
func (s *Server) StartAutoTLS() error {
11✔
138
        if s.autocertManager == nil {
12✔
139
                return fmt.Errorf("autocert manager not configured")
1✔
140
        }
1✔
141

142
        s.logger.Info("Starting server with AutoTLS...")
10✔
143

10✔
144
        errCh := make(chan error, 4)
10✔
145
        httpReady := make(chan struct{})
10✔
146

10✔
147
        if s.server == nil {
11✔
148
                close(httpReady)
1✔
149
        }
1✔
150

151
        // Start HTTP server for ACME challenges and redirects
152
        if s.server != nil {
19✔
153
                go func() {
18✔
154
                        // Create a new server for HTTP with autocert handler
9✔
155
                        httpServer := &http.Server{
9✔
156
                                Addr:    s.server.Addr,
9✔
157
                                Handler: s.autocertManager.HTTPHandler(s.createHTTPSRedirectHandler()),
9✔
158
                        }
9✔
159

9✔
160
                        ln, err := net.Listen("tcp", httpServer.Addr)
9✔
161
                        if err != nil {
12✔
162
                                s.logger.Error("Failed to bind HTTP listener", log.E(err))
3✔
163
                                errCh <- err
3✔
164
                                return
3✔
165
                        }
3✔
166

167
                        s.logger.Info("Starting HTTP server for ACME challenges and redirects",
6✔
168
                                log.F("addr", fmtHTTPAddr(httpServer.Addr)))
6✔
169
                        close(httpReady)
6✔
170
                        errCh <- httpServer.Serve(ln)
6✔
171
                }()
172
        }
173

174
        certReady := make(chan struct{})
10✔
175
        var certOnce sync.Once
10✔
176
        signalCertReady := func() {
17✔
177
                certOnce.Do(func() {
14✔
178
                        s.logger.Info("AutoTLS certificate is ready")
7✔
179
                        close(certReady)
7✔
180
                })
7✔
181
        }
182

183
        // Start HTTPS server with autocert
184
        if s.tlsServer != nil {
20✔
185
                go func() {
20✔
186
                        // Configure TLS with autocert
10✔
187
                        if s.tlsServer.TLSConfig == nil {
10✔
188
                                s.tlsServer.TLSConfig = &tls.Config{}
×
189
                        }
×
190
                        s.tlsServer.TLSConfig.GetCertificate = func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
12✔
191
                                cert, err := s.autocertManager.GetCertificate(hello)
2✔
192
                                if err == nil {
4✔
193
                                        // Signal that cert is ready (first successful retrieval)
2✔
194
                                        signalCertReady()
2✔
195
                                }
2✔
196
                                return cert, err
2✔
197
                        }
198

199
                        s.logger.Info("Starting HTTPS server with AutoTLS",
10✔
200
                                log.F("addr", fmtHTTPSAddr(s.tlsServer.Addr)))
10✔
201
                        errCh <- s.tlsServer.ListenAndServeTLS("", "")
10✔
202
                }()
203
        }
204

205
        // Warm-up goroutine: proactively fetch certificate for HTTP/3/WebTransport
206
        if s.http3Server != nil || s.webTransportServer != nil {
20✔
207
                go func() {
20✔
208
                        <-httpReady
10✔
209
                        hostnames := s.autocertManager.Hostnames()
10✔
210
                        if len(hostnames) == 0 {
11✔
211
                                s.logger.Error("AutocertManager returned no hostnames, cannot warm up certificate")
1✔
212
                                return
1✔
213
                        }
1✔
214

215
                        hello := &tls.ClientHelloInfo{ServerName: hostnames[0]}
6✔
216

6✔
217
                        // Attempt immediately before starting the ticker loop
6✔
218
                        // so a cached cert on restart doesn't incur a 2-second delay
6✔
219
                        cert, err := s.autocertManager.GetCertificate(hello)
6✔
220
                        if err == nil && cert != nil {
11✔
221
                                signalCertReady()
5✔
222
                                return
5✔
223
                        }
5✔
224
                        s.logger.Debug("Certificate not yet ready on first attempt, starting poll loop...", log.E(err))
1✔
225

1✔
226
                        ticker := time.NewTicker(2 * time.Second)
1✔
227
                        defer ticker.Stop()
1✔
228
                        timeout := time.After(5 * time.Minute)
1✔
229

1✔
230
                        for {
2✔
231
                                select {
1✔
UNCOV
232
                                case <-ticker.C:
×
UNCOV
233
                                        cert, err := s.autocertManager.GetCertificate(hello)
×
UNCOV
234
                                        if err != nil {
×
UNCOV
235
                                                s.logger.Debug("Certificate not yet ready, retrying...", log.E(err))
×
UNCOV
236
                                                continue
×
237
                                        }
238
                                        if cert != nil {
×
239
                                                signalCertReady()
×
240
                                                return
×
241
                                        }
×
242
                                case <-timeout:
×
243
                                        s.logger.Error("Timed out waiting for AutoTLS certificate")
×
244
                                        return
×
245
                                }
246
                        }
247
                }()
248
        }
249

250
        // Start HTTP/3 server with autocert if supported (after cert is ready)
251
        if s.http3Server != nil {
18✔
252
                if h3Autocert, ok := s.http3Server.(config.HTTP3ServerWithAutocert); ok {
15✔
253
                        go func() {
14✔
254
                                s.logger.Info("Waiting for certificate before starting HTTP/3...")
7✔
255
                                <-certReady
7✔
256
                                s.logger.Info("Starting HTTP/3 server with AutoTLS")
7✔
257
                                errCh <- h3Autocert.ListenAndServeTLSWithAutocert(s.autocertManager)
7✔
258
                        }()
7✔
259
                }
260
        }
261

262
        // Start WebTransport server with autocert if supported (after cert is ready)
263
        if s.webTransportServer != nil {
14✔
264
                if wtAutocert, ok := s.webTransportServer.(config.WebTransportServerWithAutocert); ok {
7✔
265
                        go func() {
6✔
266
                                s.logger.Info("Waiting for certificate before starting WebTransport...")
3✔
267
                                <-certReady
3✔
268
                                s.logger.Info("Starting WebTransport server with AutoTLS")
3✔
269
                                errCh <- wtAutocert.ListenAndServeTLSWithAutocert(s.autocertManager)
3✔
270
                        }()
3✔
271
                }
272
        }
273

274
        return <-errCh
10✔
275
}
276

277
// ListenerTLSAddr returns the network address that the HTTPS server is listening on.
278
// If a TLS listener is configured, it returns the listener's actual address.
279
// If no TLS listener is configured but a TLS server is configured, it returns the server's configured address.
280
// If neither is configured, it returns an empty string.
281
//
282
// This method is thread-safe and can be called concurrently.
283
// The returned address includes both host and port (e.g., "127.0.0.1:8443").
284
func (s *Server) ListenerTLSAddr() string {
15✔
285
        s.mu.RLock()
15✔
286
        defer s.mu.RUnlock()
15✔
287

15✔
288
        if s.tlsListener != nil {
16✔
289
                return s.tlsListener.Addr().String()
1✔
290
        }
1✔
291

292
        if s.tlsServer != nil {
27✔
293
                return s.tlsServer.Addr
13✔
294
        }
13✔
295

296
        return ""
1✔
297
}
298

299
// createHTTPSRedirectHandler creates an HTTP handler that redirects all requests
300
// to their HTTPS equivalent. This handler is used by the HTTP server when
301
// running in AutoTLS mode to ensure all traffic is encrypted.
302
//
303
// The redirect preserves the original request path and query parameters.
304
// Returns an http.Handler that performs permanent redirects (301) to HTTPS.
305
func (s *Server) createHTTPSRedirectHandler() http.Handler {
10✔
306
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
11✔
307
                // Build HTTPS URL by copying the URL and changing scheme
1✔
308
                target := *r.URL
1✔
309
                target.Scheme = "https"
1✔
310
                target.Host = r.Host
1✔
311

1✔
312
                httpsURL := target.String()
1✔
313
                s.logger.Debug("Redirecting HTTP to HTTPS",
1✔
314
                        log.F("from", r.URL.String()),
1✔
315
                        log.F("to", httpsURL))
1✔
316
                http.Redirect(w, r, httpsURL, http.StatusMovedPermanently)
1✔
317
        })
1✔
318
}
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