-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhandler.go
More file actions
347 lines (319 loc) · 12 KB
/
Copy pathhandler.go
File metadata and controls
347 lines (319 loc) · 12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
package servekit
import (
"context"
"errors"
"net/http"
"time"
)
// HandlerFunc is the function form accepted by Server.Handle.
//
// The request passed to HandlerFunc carries any endpoint timeout set with
// WithEndpointTimeout in r.Context(). Returning a non-nil error delegates
// response writing to the server ErrorEncoder.
type HandlerFunc func(r *http.Request) (any, error)
// Handle registers a method/path endpoint backed by a HandlerFunc.
//
// Handle applies endpoint policy before endpoint middleware and h. The policy
// installs the endpoint timeout and body limit, then runs the auth checks.
// Successful results are encoded with the server ResponseEncoder unless
// WithEndpointResponseEncoder overrides it.
// Errors from h or the encoder are sent through the server ErrorEncoder. If a
// success response has already been committed, the error path may not be able
// to replace it cleanly.
func (s *Server) Handle(method, path string, h HandlerFunc, opts ...EndpointOption) {
cfg := endpointConfig{}
for _, opt := range opts {
opt(&cfg)
}
base := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
payload, err := h(r)
if err != nil {
_ = s.errorResponse(w, r, err)
return
}
encoder := s.responseEncoder
if cfg.responseOverride != nil {
encoder = cfg.responseOverride
}
if err := encoder(w, r, payload); err != nil {
_ = s.errorResponse(w, r, err)
}
})
inner := Chain(base, cfg.middlewares...)
final := s.wrapEndpoint(inner, cfg)
s.register(method, path, final, cfg)
}
// HandleHTTP registers a method/path endpoint backed by a raw http.Handler.
//
// HandleHTTP applies endpoint policy before endpoint middleware and h. The
// policy installs the endpoint timeout and body limit, then runs the auth
// checks. Use HandleHTTP when you need direct control over response writing
// while still using Servekit middleware composition and endpoint options.
//
// Optional writer capabilities such as Flush and Hijack are not guaranteed by
// http.ResponseWriter itself. They depend on what the underlying concrete
// writer supports at runtime. Servekit preserves those capabilities when they
// are present so HandleHTTP remains a credible raw escape hatch for streaming,
// upgrade, proxy, and other raw-response use cases.
func (s *Server) HandleHTTP(method, path string, h http.Handler, opts ...EndpointOption) {
cfg := endpointConfig{}
for _, opt := range opts {
opt(&cfg)
}
inner := Chain(h, cfg.middlewares...)
final := s.wrapEndpoint(inner, cfg)
s.register(method, path, final, cfg)
}
// wrapEndpoint applies per-endpoint timeout, body-limit, and auth behavior
// around h.
//
// The returned handler updates the request context and body before auth and h
// so Handle and HandleHTTP observe the same endpoint-level policy.
func (s *Server) wrapEndpoint(h http.Handler, cfg endpointConfig) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if cfg.timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, cfg.timeout)
defer cancel()
}
r = r.WithContext(ctx)
effectiveLimit := s.requestBodyLimit
if cfg.bodyLimit != 0 {
effectiveLimit = cfg.bodyLimit
}
if effectiveLimit > 0 {
r.Body = http.MaxBytesReader(w, r.Body, effectiveLimit)
}
if cfg.requireAuth != nil && !cfg.requireAuth(r) {
markRequestAuthRejected(r)
_ = s.errorResponse(w, r, HTTPError{StatusCode: http.StatusUnauthorized, Message: "unauthorized"})
return
}
if cfg.requireAuthGate != nil {
if err := cfg.requireAuthGate(r); err != nil {
markRequestAuthRejected(r)
_ = s.errorResponse(w, r, err)
return
}
}
h.ServeHTTP(w, r)
switch ctx.Err() {
case context.DeadlineExceeded:
markRequestTimedOut(r)
case context.Canceled:
markRequestCanceled(r)
}
})
}
// register validates and installs a fully prepared route into the server mux.
//
// The wrapper records the matched path pattern so outer observability
// middleware can observe the final route after mux dispatch has run.
func (s *Server) register(method, path string, h http.Handler, cfg endpointConfig) {
validateRoute(method, path)
pattern := method + " " + path
base := h
h = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Seed the matched-route holder here so access logs and other outer
// middleware can observe the final route even when OTel is disabled.
r = withMatchedRoute(r)
setMatchedRoutePath(r, path)
base.ServeHTTP(w, r)
})
if cfg.skipTelemetry {
if s.skipTelemetryPatterns == nil {
s.skipTelemetryPatterns = make(map[string]struct{})
}
s.skipTelemetryPatterns[pattern] = struct{}{}
h = Chain(h, Middleware(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, markSkipTelemetry(r))
})
}))
}
if cfg.skipAccessLog {
h = Chain(h, SkipAccessLog())
}
s.mux.Handle(pattern, h)
}
// validateRoute rejects obviously broken route definitions at registration time.
func validateRoute(method, path string) {
if method == "" {
panic("servekit: route method must not be empty")
}
if path == "" {
panic("servekit: route path must not be empty")
}
}
// ReadinessCheck is a lightweight readiness predicate over local or cached
// state. Returning nil allows readiness to proceed. Returning an error marks
// the service not ready; Servekit logs the error at debug level but does not
// include it verbatim in the public response. Active dependency checks should
// run outside the HTTP probe path.
type ReadinessCheck func(context.Context) error
// HTTPError carries an HTTP status code alongside an underlying error.
//
// Both HTTPError values and pointers are recognized through wrapped error
// chains. Use HTTPError (or Error) when handlers need explicit control over
// status mapping instead of relying on the default 500/504 behavior.
// StatusCode values from 200 through 599 are used as final response statuses.
// A zero or negative value leaves status selection to the default mapping;
// other values fail closed to HTTP 500.
type HTTPError struct {
StatusCode int // StatusCode is the final HTTP status requested for this error.
Message string // Message is the client-facing error text.
Err error // Err is the wrapped underlying cause, when present.
}
// Error implements error.
func (e HTTPError) Error() string {
if e.Err != nil {
return e.Message + ": " + e.Err.Error()
}
return e.Message
}
// Unwrap returns the wrapped cause for errors.Is/errors.As.
func (e HTTPError) Unwrap() error {
return e.Err
}
// Error constructs an HTTPError value.
func Error(status int, message string, err error) error {
return HTTPError{StatusCode: status, Message: message, Err: err}
}
// asHTTPError returns the first HTTPError in err's tree, preserving the same
// depth-first order as errors.As while accepting both value and pointer forms.
// A nil result with ok set reports a typed-nil *HTTPError.
func asHTTPError(err error) (*HTTPError, bool) {
if err == nil {
return nil, false
}
switch httpErr := err.(type) {
case HTTPError:
return &httpErr, true
case *HTTPError:
return httpErr, true
}
// Preserve the custom conversion behavior supported by errors.As before
// descending into ordinary wrapped errors.
if matcher, ok := err.(interface{ As(any) bool }); ok {
var value HTTPError
if matcher.As(&value) {
return &value, true
}
var pointer *HTTPError
if matcher.As(&pointer) {
return pointer, true
}
}
switch wrapped := err.(type) {
case interface{ Unwrap() error }:
return asHTTPError(wrapped.Unwrap())
case interface{ Unwrap() []error }:
for _, child := range wrapped.Unwrap() {
if httpErr, ok := asHTTPError(child); ok {
return httpErr, true
}
}
}
return nil, false
}
func validHTTPErrorStatus(status int) bool {
return status >= 200 && status <= 599
}
func statusFromError(err error) int {
if httpErr, ok := asHTTPError(err); ok {
if httpErr == nil {
return http.StatusInternalServerError
}
if httpErr.StatusCode > 0 {
if validHTTPErrorStatus(httpErr.StatusCode) {
return httpErr.StatusCode
}
return http.StatusInternalServerError
}
}
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
return http.StatusGatewayTimeout
}
var maxBytesErr *http.MaxBytesError
if errors.As(err, &maxBytesErr) {
return http.StatusRequestEntityTooLarge
}
return http.StatusInternalServerError
}
// EndpointOption configures per-endpoint behavior for Handle and HandleHTTP.
type EndpointOption func(*endpointConfig)
// endpointConfig holds the accumulated endpoint option state during route
// registration.
type endpointConfig struct {
timeout time.Duration
bodyLimit int64
middlewares []Middleware
requireAuth func(*http.Request) bool
requireAuthGate func(*http.Request) error
responseOverride ResponseEncoder
skipAccessLog bool
skipTelemetry bool
}
// WithEndpointMiddleware appends middleware applied only to that endpoint.
//
// Endpoint middleware runs after endpoint timeout, body-limit, and auth policy,
// and wraps the handler before global server middleware is applied by
// Server.Handler.
func WithEndpointMiddleware(mw ...Middleware) EndpointOption {
return func(cfg *endpointConfig) { cfg.middlewares = append(cfg.middlewares, mw...) }
}
// WithEndpointTimeout sets a per-endpoint context timeout.
//
// Endpoint middleware and the handler receive the resulting context. A timeout
// of zero leaves the incoming request context unchanged.
func WithEndpointTimeout(timeout time.Duration) EndpointOption {
return func(cfg *endpointConfig) { cfg.timeout = timeout }
}
// WithBodyLimit sets the maximum number of bytes Servekit will read from
// the request body for this endpoint. A value of -1 disables the limit
// entirely. The default is the server-wide WithRequestBodyLimit value
// (4 MiB unless overridden).
//
// Endpoint middleware and the handler share the limited body. When the limit is
// exceeded, net/http returns an *http.MaxBytesError. Servekit maps errors
// returned by HandlerFunc to HTTP 413 Request Entity Too Large; raw endpoint
// middleware and HandleHTTP handlers remain responsible for their own response.
func WithBodyLimit(n int64) EndpointOption {
return func(cfg *endpointConfig) { cfg.bodyLimit = n }
}
// WithAuthCheck installs an authorization gate for the endpoint.
//
// When check returns false, Handle and HandleHTTP respond with HTTP 401 via the
// current ErrorEncoder and do not invoke endpoint middleware or the handler.
// This convenience form always returns HTTP 401. Use WithAuthGate when you need
// control over the returned status or message.
func WithAuthCheck(check func(*http.Request) bool) EndpointOption {
return func(cfg *endpointConfig) { cfg.requireAuth = check }
}
// WithAuthGate installs an error-returning auth gate for the endpoint.
//
// When fn returns a non-nil error, Handle and HandleHTTP pass that error
// directly to the current ErrorEncoder and do not invoke endpoint middleware or
// the handler. Return an HTTPError value or pointer, or use Error(...), when you
// need explicit control over the response status and message.
func WithAuthGate(fn func(*http.Request) error) EndpointOption {
return func(cfg *endpointConfig) { cfg.requireAuthGate = fn }
}
// WithEndpointResponseEncoder overrides success encoding for one endpoint.
//
// This option applies only to Handle. If the encoder returns an error, that
// error is delegated to the server ErrorEncoder.
func WithEndpointResponseEncoder(encoder ResponseEncoder) EndpointOption {
return func(cfg *endpointConfig) { cfg.responseOverride = encoder }
}
// WithSkipAccessLog suppresses AccessLog output for one endpoint.
//
// This is useful for high-frequency probes such as /healthz and /readyz.
func WithSkipAccessLog() EndpointOption {
return func(cfg *endpointConfig) { cfg.skipAccessLog = true }
}
// WithSkipTelemetry suppresses built-in OTel tracing and metrics for one endpoint.
func WithSkipTelemetry() EndpointOption {
return func(cfg *endpointConfig) { cfg.skipTelemetry = true }
}