-
-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathgroup.go
More file actions
356 lines (304 loc) · 12.4 KB
/
group.go
File metadata and controls
356 lines (304 loc) · 12.4 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
348
349
350
351
352
353
354
355
356
// Package routegroup provides a way to group routes and applies middleware to them.
// Works with the standard library's http.ServeMux.
package routegroup
import (
"net/http"
"regexp"
"strings"
)
// Bundle represents a group of routes with associated middleware.
type Bundle struct {
mux *http.ServeMux // the underlying mux to register the routes to
basePath string // base path for the group
middlewares []func(http.Handler) http.Handler // middlewares stack
// optional custom 404 handler
notFound http.HandlerFunc
// root points to the root bundle for global middleware application.
// for the root bundle, root == nil.
root *Bundle
// routesLocked indicates that routes have been registered on the root bundle
// and no further root-level middlewares may be added.
routesLocked bool
// rootCount captures how many root middlewares were present when this bundle
// was created. Used to avoid double-applying root middlewares for per-route wrapping.
rootCount int
}
// New creates a new Group.
func New(mux *http.ServeMux) *Bundle {
return &Bundle{mux: mux}
}
// Mount creates a new group with a specified base path.
func Mount(mux *http.ServeMux, basePath string) *Bundle {
return &Bundle{mux: mux, basePath: basePath}
}
// ServeHTTP implements the http.Handler interface
func (b *Bundle) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// resolve the root bundle (where global middlewares live).
root := b
if b.root != nil {
root = b.root
}
// get the handler and pattern for this request
_, pattern := b.mux.Handler(r)
// if a pattern was found, create a shallow copy of the request with the pattern set
// this allows global middlewares to see the pattern before mux.ServeHTTP is called
if pattern != "" {
r2 := *r
r2.Pattern = pattern
r = &r2
}
// create a handler that will let the mux do its routing (including setting path parameters)
// but intercept 404s to use custom handler if provided
muxHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if pattern == "" && root.notFound != nil {
// no route matched, need to check if it's a true 404 or a 405
// probe the mux to see what status it would return
probe := &statusRecorder{status: http.StatusOK}
b.mux.ServeHTTP(probe, r)
// if mux wants to return 405 (Method Not Allowed), let it handle the request
// to preserve the proper 405 response and Allow header
if probe.status == http.StatusMethodNotAllowed {
b.mux.ServeHTTP(w, r)
return
}
// it's a true 404, use custom handler
root.notFound.ServeHTTP(w, r)
return
}
// let the mux handle the request normally (this sets path parameters)
b.mux.ServeHTTP(w, r)
})
// apply root (global) middlewares around the mux handler and serve the request.
root.wrapGlobal(muxHandler).ServeHTTP(w, r)
}
// Group creates a new group with the same middleware stack as the original on top of the existing bundle.
func (b *Bundle) Group() *Bundle {
return b.clone() // copy the middlewares to avoid modifying the original
}
// Mount creates a new group with a specified base path on top of the existing bundle.
func (b *Bundle) Mount(basePath string) *Bundle {
g := b.clone() // copy the middlewares to avoid modifying the original
g.basePath += basePath
return g
}
// Use adds middleware(s) to the Group.
// Middlewares are executed in the order they are added.
// Note: Root-level middlewares (added to the root bundle) have access to the matched
// route pattern via r.Pattern, but execute before path parameters are parsed.
// Therefore, r.PathValue() will return empty strings in root middlewares.
// Middlewares on mounted groups execute after routing and have full access to path values.
func (b *Bundle) Use(middleware func(http.Handler) http.Handler, more ...func(http.Handler) http.Handler) {
// disallow adding middlewares after any routes have been registered on this bundle.
if b.routesLocked {
panic("routegroup: Use called after routes were registered on this bundle; add middlewares before registering routes or use Group/With for scoped middleware")
}
b.middlewares = append(b.middlewares, middleware)
b.middlewares = append(b.middlewares, more...)
}
// With adds new middleware(s) to the Group and returns a new Group with the updated middleware stack.
// The With method is similar to Use, but instead of modifying the current Group,
// it returns a new Group instance with the added middleware(s).
// This allows for creating chain of middleware without affecting the original Group.
func (b *Bundle) With(middleware func(http.Handler) http.Handler, more ...func(http.Handler) http.Handler) *Bundle {
newMiddlewares := make([]func(http.Handler) http.Handler, len(b.middlewares), len(b.middlewares)+len(more)+1)
copy(newMiddlewares, b.middlewares)
newMiddlewares = append(newMiddlewares, middleware)
newMiddlewares = append(newMiddlewares, more...)
// preserve root pointer and rootCount
nb := &Bundle{mux: b.mux, basePath: b.basePath, middlewares: newMiddlewares, root: b.root, rootCount: b.rootCount}
if nb.root == nil {
// b is the root, so all b's middlewares are root middlewares
nb.root = b
nb.rootCount = len(b.middlewares)
}
return nb
}
// Handle adds a new route to the Group's mux, applying all middlewares to the handler.
func (b *Bundle) Handle(pattern string, handler http.Handler) {
b.lockRoot() // lock root on first route registration
// for file server paths (ending with /), preserve the pattern as-is
if strings.HasSuffix(pattern, "/") {
fullPath := b.basePath + pattern
b.mux.Handle(fullPath, b.wrapMiddleware(handler))
return
}
b.register(pattern, handler.ServeHTTP)
}
// HandleFiles is a helper to serve static files from a directory
func (b *Bundle) HandleFiles(pattern string, root http.FileSystem) {
b.lockRoot() // lock root on first route registration
// normalize pattern to always have trailing slash
if !strings.HasSuffix(pattern, "/") {
pattern += "/"
}
// build the full path for registration
fullPath := b.basePath + pattern
if pattern == "/" && b.basePath == "" {
// root case - serve directly without stripping
b.mux.Handle("/", b.wrapMiddleware(http.FileServer(root)))
return
}
// for both mounted groups and prefixed paths, strip the fullPath
handler := http.StripPrefix(strings.TrimSuffix(fullPath, "/"), http.FileServer(root))
b.mux.Handle(fullPath, b.wrapMiddleware(handler))
}
// HandleFunc registers the handler function for the given pattern to the Group's mux.
// The handler is wrapped with the Group's middlewares.
func (b *Bundle) HandleFunc(pattern string, handler http.HandlerFunc) {
b.register(pattern, handler)
}
// Handler returns the handler and the pattern that matches the request.
// It always returns a non-nil handler, see http.ServeMux.Handler documentation for details.
func (b *Bundle) Handler(r *http.Request) (h http.Handler, pattern string) {
return b.mux.Handler(r)
}
// DisableNotFoundHandler used to disable auto-registration of a catch-all 404.
//
// Deprecated: now a no-op retained for API compatibility.
func (b *Bundle) DisableNotFoundHandler() {}
// NotFoundHandler sets a custom handler for any unmatched routes (404 responses).
// Note: This handler is only used for true 404s. Requests to valid paths with
// incorrect HTTP methods will still return 405 Method Not Allowed with Allow header.
func (b *Bundle) NotFoundHandler(handler http.HandlerFunc) {
// always set on the root bundle so custom 404 works regardless of which bundle serves.
if b.root != nil {
b.root.notFound = handler
return
}
b.notFound = handler
}
// matches non-space characters, spaces, then anything, i.e. "GET /path/to/resource"
var reGo122 = regexp.MustCompile(`^(\S+)\s+(.+)$`)
func (b *Bundle) register(pattern string, handler http.HandlerFunc) {
b.lockRoot() // lock root on first route registration
matches := reGo122.FindStringSubmatch(pattern)
var path, method string
if len(matches) > 2 { // path in the form "GET /path/to/resource"
method = matches[1]
path = matches[2]
pattern = method + " " + b.basePath + path
} else { // path is just "/path/to/resource"
path = pattern
pattern = b.basePath + pattern
// method is not set intentionally here, the request pattern had no method part
}
// if the pattern is the root path on / change it to /{$}
// this keeps handling the root request without becoming a catch-all
if pattern == "/" || path == "/" {
if method != "" { // preserve the method part if it was set
pattern = method + " " + b.basePath + "/{$}"
} else {
pattern = b.basePath + "/{$}" // no method part, just the path
}
}
b.mux.HandleFunc(pattern, b.wrapMiddleware(handler).ServeHTTP)
}
// Route allows for configuring the Group inside the configureFn function.
// When called on the root bundle, it automatically creates a new group to avoid
// accidentally modifying the root bundle's middleware stack.
func (b *Bundle) Route(configureFn func(*Bundle)) {
// if called on root bundle, auto-create a group for better UX
if b.root == nil {
child := b.Group()
configureFn(child)
// if child registered routes, lock root too to prevent Use() after routes
if child.routesLocked {
b.routesLocked = true
}
return
}
configureFn(b)
}
// HandleRoot adds a handler for the group's root path without trailing slash.
// This avoids the 301 redirect that would occur with a "/" pattern.
// Method parameter can be empty to register for all HTTP methods.
func (b *Bundle) HandleRoot(method string, handler http.Handler) {
b.lockRoot() // lock root on first route registration
// for empty base path, use "/" to match the root
pattern := b.basePath
if pattern == "" {
pattern = "/"
}
// add method if specified
if method != "" {
pattern = method + " " + pattern
}
b.mux.Handle(pattern, b.wrapMiddleware(handler))
}
// HandleRootFunc is like HandleRoot but takes a handler function.
func (b *Bundle) HandleRootFunc(method string, handler http.HandlerFunc) {
b.lockRoot() // lock root on first route registration
// for empty base path, use "/" to match the root
pattern := b.basePath
if pattern == "" {
pattern = "/"
}
// add method if specified
if method != "" {
pattern = method + " " + pattern
}
b.mux.HandleFunc(pattern, b.wrapMiddleware(handler).ServeHTTP)
}
// wrapMiddleware applies the registered middlewares to a handler.
func (b *Bundle) wrapMiddleware(handler http.Handler) http.Handler {
// root bundle: don't apply middlewares here, they're applied globally in ServeHTTP
if b.root == nil {
return handler
}
// child bundle: apply only middlewares added after mounting (exclude inherited root middlewares)
start := b.rootCount
if start > len(b.middlewares) {
start = len(b.middlewares) // safety: ensure start doesn't exceed bounds
}
for i := len(b.middlewares) - 1; i >= start; i-- {
handler = b.middlewares[i](handler)
}
return handler
}
func (b *Bundle) clone() *Bundle {
middlewares := make([]func(http.Handler) http.Handler, len(b.middlewares))
copy(middlewares, b.middlewares)
// preserve root pointer and rootCount
nb := &Bundle{mux: b.mux, basePath: b.basePath, middlewares: middlewares, root: b.root, rootCount: b.rootCount}
if nb.root == nil {
// b is the root, so all b's middlewares are root middlewares
nb.root = b
nb.rootCount = len(b.middlewares)
}
return nb
}
// Wrap directly wraps the handler with the provided middleware(s).
func Wrap(handler http.Handler, mw1 func(http.Handler) http.Handler, mws ...func(http.Handler) http.Handler) http.Handler {
for i := len(mws) - 1; i >= 0; i-- {
handler = mws[i](handler)
}
return mw1(handler) // apply the first middleware
}
// wrapGlobal applies only the root bundle's middlewares to the provided handler.
func (b *Bundle) wrapGlobal(handler http.Handler) http.Handler {
// resolve root bundle
root := b
if b.root != nil {
root = b.root
}
for i := len(root.middlewares) - 1; i >= 0; i-- {
handler = root.middlewares[i](handler)
}
return handler
}
// lockRoot marks this bundle as having registered routes.
func (b *Bundle) lockRoot() { b.routesLocked = true }
// statusRecorder is a minimal ResponseWriter that only records the status code.
// Used to probe what status the mux would return without actually writing a response.
type statusRecorder struct {
status int
}
func (r *statusRecorder) Header() http.Header {
return make(http.Header)
}
func (r *statusRecorder) Write([]byte) (int, error) {
return 0, nil
}
func (r *statusRecorder) WriteHeader(status int) {
r.status = status
}