forked from dbubel/intake
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathintake.go
More file actions
224 lines (204 loc) · 7.94 KB
/
Copy pathintake.go
File metadata and controls
224 lines (204 loc) · 7.94 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
// Package intake implements a simple HTTP router with middleware support.
// It provides a lightweight framework for building HTTP services with support for
// middleware, panic recovery, and graceful shutdown.
package intake
import (
"context"
"errors"
"fmt"
"net/http"
"os"
"os/signal"
"slices"
"syscall"
"time"
)
// MiddleWare defines a function that wraps an http.HandlerFunc with additional behavior.
// Middleware functions can be used to add cross-cutting concerns such as logging,
// authentication, request tracing, or any other functionality that should be applied
// to multiple endpoints.
type MiddleWare func(http.HandlerFunc) http.HandlerFunc
// Intake represents an HTTP router with middleware and panic recovery support.
// It provides a simple API for registering routes, applying middleware, and
// handling HTTP requests. The Intake struct encapsulates all the functionality
// needed to build and run an HTTP service.
type Intake struct {
// Mux is the underlying HTTP request multiplexer
Mux *http.ServeMux
// PanicHandler handles any panics that occur during request processing
PanicHandler func(http.ResponseWriter, *http.Request, any)
// GlobalMiddleware contains middleware applied to all routes
GlobalMiddleware []MiddleWare
// registeredRoutes maps paths to their HTTP methods
registeredRoutes map[string][]string
}
// New creates a new Intake instance with initialized maps and slices.
// It sets up the internal data structures needed for routing and middleware
// management. This function should be called to create a new router before
// registering any routes or middleware.
func New() *Intake {
return &Intake{
GlobalMiddleware: make([]MiddleWare, 0),
Mux: http.NewServeMux(),
registeredRoutes: make(map[string][]string),
}
}
// SetPanicHandler sets a custom panic handler function that will be called
// when a panic occurs during request processing. This provides a way to
// recover from panics and return appropriate error responses instead of
// letting the server crash.
//
// Parameters:
// - handler: The panic handler function that takes an http.ResponseWriter,
// an *http.Request, and the recovered panic value.
func (a *Intake) SetPanicHandler(handler func(http.ResponseWriter, *http.Request, any)) {
a.PanicHandler = handler
}
// AddGlobalMiddleware adds middleware that will be applied to all routes.
// Global middleware must be added before registering routes. The middleware
// functions are executed in the order they are added, with the first added
// middleware being the outermost wrapper around the handler function.
//
// Parameters:
// - mw: The middleware function to add to the global middleware chain.
func (a *Intake) AddGlobalMiddleware(mw MiddleWare) {
a.GlobalMiddleware = append(a.GlobalMiddleware, mw)
}
// AddEndpoints registers multiple endpoints at once.
// This is a convenience method that allows registering multiple endpoints
// in a single call, which can improve code readability when setting up
// multiple related routes.
//
// Parameters:
// - e: A variadic parameter of Endpoints slices to register.
func (a *Intake) AddEndpoints(e ...Endpoints) {
for x := range e {
for i := range e[x] {
a.AddEndpoint(e[x][i].Verb, e[x][i].Path, e[x][i].EndpointHandler, e[x][i].MiddlewareHandlers...)
}
}
}
// AddEndpoint registers a new route with the specified HTTP method and path.
// It applies both global and route-specific middleware to the handler. The
// middleware is applied in order, with global middleware being applied before
// route-specific middleware.
//
// Parameters:
// - verb: The HTTP method (GET, POST, PUT, DELETE, etc.)
// - path: The URL path to register the handler for. Go 1.22 path parameters
// like /users/{id} are supported.
// - finalHandler: The handler function that will process the request
// - middleware: Optional route-specific middleware functions
func (a *Intake) AddEndpoint(verb string, path string, finalHandler http.HandlerFunc, middleware ...MiddleWare) {
// Store the route in our registry, avoiding duplicates.
if methods, exists := a.registeredRoutes[path]; exists {
if !slices.Contains(methods, verb) {
a.registeredRoutes[path] = append(methods, verb)
}
} else {
a.registeredRoutes[path] = []string{verb}
}
handlerKey := fmt.Sprintf("%s %s", verb, path)
// Build route-specific chain first.
routeHandler := finalHandler
for i := len(middleware) - 1; i >= 0; i-- {
if middleware[i] != nil {
routeHandler = middleware[i](routeHandler)
}
}
// Apply global middleware in reverse order
handler := routeHandler
for i := len(a.GlobalMiddleware) - 1; i >= 0; i-- {
if a.GlobalMiddleware[i] != nil {
handler = a.GlobalMiddleware[i](handler)
}
}
// Apply panic recovery last so it wraps global and route middleware.
// The handler check is dynamic so SetPanicHandler can be called either
// before or after AddEndpoint.
inner := handler
handler = func(w http.ResponseWriter, r *http.Request) {
if a.PanicHandler != nil {
defer func() {
if err := recover(); err != nil {
a.PanicHandler(w, r, err)
}
}()
}
inner(w, r)
}
a.Mux.HandleFunc(handlerKey, handler)
}
// Run starts the HTTP server and handles graceful shutdown on SIGINT/SIGTERM.
// This method blocks until the server is shut down either by an error or by
// receiving a termination signal. When a signal is received, the server attempts
// to gracefully shut down, allowing in-flight requests to complete within a
// timeout period.
//
// Parameters:
// - server: The configured http.Server instance to run
//
// Returns:
// - nil on clean shutdown after a signal, or the underlying ListenAndServe /
// Shutdown error otherwise. http.ErrServerClosed is treated as nil.
func (a *Intake) Run(server *http.Server) error {
serverErrors := make(chan error, 1)
osSignals := make(chan os.Signal, 1)
signal.Notify(osSignals, syscall.SIGINT, syscall.SIGTERM)
defer signal.Stop(osSignals)
go func() {
serverErrors <- server.ListenAndServe()
}()
select {
case err := <-serverErrors:
if errors.Is(err, http.ErrServerClosed) {
return nil
}
return fmt.Errorf("server error: %w", err)
case <-osSignals:
ctx, cancel := context.WithTimeout(context.Background(), time.Second*60)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
if closeErr := server.Close(); closeErr != nil {
return fmt.Errorf("graceful shutdown failed: %w; force close also failed: %v", err, closeErr)
}
return fmt.Errorf("graceful shutdown failed, forced close: %w", err)
}
return nil
}
}
// GetRoutes returns a map of paths to their supported HTTP methods.
// This can be useful for debugging or for generating documentation about
// the available endpoints. The returned map is a copy of the internal
// route registry, so modifications to it will not affect the router.
//
// Returns:
// - A map where keys are URL paths and values are slices of HTTP methods
// supported by each path.
func (a *Intake) GetRoutes() map[string][]string {
routes := make(map[string][]string)
for path, methods := range a.registeredRoutes {
routes[path] = slices.Clone(methods)
}
return routes
}
// AddOptionsEndpoints automatically adds HTTP OPTIONS handlers for all registered routes.
// This is particularly useful for CORS preflight requests. The generated OPTIONS handlers
// will respond with a 204 No Content status, and the actual CORS headers will be set by
// any CORS middleware that has been applied.
//
// This method should be called after all other routes have been registered.
func (a *Intake) AddOptionsEndpoints() {
for path, methods := range a.registeredRoutes {
// Skip if OPTIONS is already registered for this path
if slices.Contains(methods, http.MethodOptions) {
continue
}
// Create an OPTIONS handler that just returns 204 No Content
optionsHandler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}
// Register the OPTIONS handler for this path
a.AddEndpoint(http.MethodOptions, path, optionsHandler)
}
}