Skip to content

Commit 550f4a9

Browse files
authored
add middleware edit for req headers (#58)
* add middleware edit for req headers * remove the sort from the examples * add unit tests for the request header editor
1 parent 7f66c89 commit 550f4a9

File tree

4 files changed

+1130
-7
lines changed

4 files changed

+1130
-7
lines changed

examples/headers_middleware/main.go

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
// Example of request/response header manipulation using a middleware.
2+
//
3+
// # Header Manipulation Example
4+
//
5+
// This example shows how to use the new request header manipulation features
6+
// alongside existing response header functionality:
7+
//
8+
// 1. Request Header Operations:
9+
// - Remove potentially sensitive headers (X-Forwarded-For)
10+
// - Add custom request headers (X-Internal-Request)
11+
// - Set specific request headers (X-Request-Source)
12+
//
13+
// 2. Response Header Operations:
14+
// - Add security headers (X-Frame-Options)
15+
// - Set custom API headers (X-API-Version)
16+
// - Remove server identification headers (Server)
17+
//
18+
// 3. Header Inspection Route:
19+
// - Displays all request headers received by the handler
20+
// - Shows how request headers are modified before reaching handlers
21+
// - Demonstrates response headers are added after handler execution
22+
//
23+
// The middleware processes headers in this order:
24+
// Request: remove → set → add (before calling handler)
25+
// Response: remove → set → add (after handler returns)
26+
package main
27+
28+
import (
29+
"context"
30+
"encoding/json"
31+
"fmt"
32+
"log/slog"
33+
"net/http"
34+
"os"
35+
"time"
36+
37+
"github.com/robbyt/go-supervisor/runnables/httpserver"
38+
"github.com/robbyt/go-supervisor/runnables/httpserver/middleware/headers"
39+
"github.com/robbyt/go-supervisor/runnables/httpserver/middleware/logger"
40+
"github.com/robbyt/go-supervisor/runnables/httpserver/middleware/metrics"
41+
"github.com/robbyt/go-supervisor/runnables/httpserver/middleware/recovery"
42+
"github.com/robbyt/go-supervisor/supervisor"
43+
)
44+
45+
const (
46+
// Port the HTTP server binds to
47+
ListenOn = ":8082"
48+
49+
// How long the supervisor waits for the HTTP server to drain before forcefully shutting down
50+
DrainTimeout = 5 * time.Second
51+
)
52+
53+
// HeaderInfo represents header information for JSON response
54+
type HeaderInfo struct {
55+
Name string `json:"name"`
56+
Values []string `json:"values"`
57+
}
58+
59+
// HeaderResponse represents the complete header inspection response
60+
type HeaderResponse struct {
61+
Message string `json:"message"`
62+
RequestHeaders []HeaderInfo `json:"request_headers"`
63+
ResponseHeaders []HeaderInfo `json:"response_headers,omitempty"`
64+
Timestamp string `json:"timestamp"`
65+
}
66+
67+
// buildRoutes sets up HTTP routes demonstrating header manipulation
68+
func buildRoutes(logHandler slog.Handler) ([]httpserver.Route, error) {
69+
// Create base middleware stack
70+
recoveryMw := recovery.New(logHandler.WithGroup("recovery"))
71+
loggingMw := logger.New(logHandler.WithGroup("headers_example"))
72+
metricsMw := metrics.New()
73+
74+
// Create comprehensive headers middleware that demonstrates both
75+
// request and response header manipulation
76+
headersMw := headers.NewWithOperations(
77+
// Request header operations (applied before handler)
78+
headers.WithRemoveRequest("X-Forwarded-For", "X-Real-IP"), // Remove proxy headers
79+
headers.WithSetRequest(headers.HeaderMap{
80+
"X-Request-Source": "go-supervisor-example", // Set request source
81+
}),
82+
headers.WithAddRequest(headers.HeaderMap{
83+
"X-Internal-Request": "true", // Mark as internal
84+
}),
85+
headers.WithAddRequestHeader("X-Processing-Time", time.Now().Format(time.RFC3339)),
86+
87+
// Response header operations (applied after handler)
88+
headers.WithRemove("Server", "X-Powered-By"), // Remove server identification
89+
headers.WithSet(headers.HeaderMap{
90+
"X-Frame-Options": "DENY", // Security header
91+
"X-API-Version": "v1.0", // API version
92+
"Content-Type": "application/json", // JSON responses
93+
}),
94+
headers.WithAdd(headers.HeaderMap{
95+
"X-Custom-Header": "go-supervisor-headers", // Custom header
96+
}),
97+
headers.WithAddHeader("X-Response-Time", time.Now().Format(time.RFC3339)),
98+
)
99+
100+
// Common middleware stack
101+
commonMw := []httpserver.HandlerFunc{
102+
recoveryMw,
103+
loggingMw,
104+
metricsMw,
105+
headersMw, // Headers middleware processes both request and response
106+
}
107+
108+
// Header inspection handler - shows request headers received after middleware processing
109+
headerInspectionHandler := func(w http.ResponseWriter, r *http.Request) {
110+
// Collect request headers
111+
var requestHeaders []HeaderInfo
112+
for name, values := range r.Header {
113+
requestHeaders = append(requestHeaders, HeaderInfo{
114+
Name: name,
115+
Values: values,
116+
})
117+
}
118+
119+
response := HeaderResponse{
120+
Message: "Header inspection complete - request headers show middleware effects",
121+
RequestHeaders: requestHeaders,
122+
Timestamp: time.Now().Format(time.RFC3339),
123+
}
124+
125+
// Response headers will be added by middleware after this handler returns
126+
jsonData, err := json.MarshalIndent(response, "", " ")
127+
if err != nil {
128+
http.Error(w, "Internal server error", http.StatusInternalServerError)
129+
return
130+
}
131+
_, err = fmt.Fprint(w, string(jsonData))
132+
if err != nil {
133+
http.Error(w, "Failed to write response", http.StatusInternalServerError)
134+
return
135+
}
136+
}
137+
138+
// Simple response handler
139+
simpleHandler := func(w http.ResponseWriter, r *http.Request) {
140+
response := map[string]any{
141+
"message": "Simple response with header manipulation",
142+
"timestamp": time.Now().Format(time.RFC3339),
143+
"note": "Check response headers - they've been modified by middleware",
144+
}
145+
jsonData, err := json.MarshalIndent(response, "", " ")
146+
if err != nil {
147+
http.Error(w, "Internal server error", http.StatusInternalServerError)
148+
return
149+
}
150+
_, err = fmt.Fprint(w, string(jsonData))
151+
if err != nil {
152+
http.Error(w, "Failed to write response", http.StatusInternalServerError)
153+
return
154+
}
155+
}
156+
157+
// Create routes
158+
headerRoute, err := httpserver.NewRouteFromHandlerFunc(
159+
"header-inspection",
160+
"/headers",
161+
headerInspectionHandler,
162+
commonMw...,
163+
)
164+
if err != nil {
165+
return nil, fmt.Errorf("failed to create header inspection route: %w", err)
166+
}
167+
168+
simpleRoute, err := httpserver.NewRouteFromHandlerFunc(
169+
"simple",
170+
"/",
171+
simpleHandler,
172+
commonMw...,
173+
)
174+
if err != nil {
175+
return nil, fmt.Errorf("failed to create simple route: %w", err)
176+
}
177+
178+
// Route with different header middleware for comparison
179+
differentHeadersMw := headers.NewWithOperations(
180+
// Different request operations
181+
headers.WithSetRequestHeader("X-Route-Specific", "different-route"),
182+
headers.WithRemoveRequest("User-Agent"),
183+
184+
// Different response operations
185+
headers.WithSetHeader("X-Route-Type", "special"),
186+
headers.WithAddHeader("X-Different-Header", "route-specific-value"),
187+
)
188+
189+
specialMw := []httpserver.HandlerFunc{
190+
recoveryMw,
191+
loggingMw,
192+
metricsMw,
193+
differentHeadersMw, // Different headers middleware
194+
}
195+
196+
specialHandler := func(w http.ResponseWriter, r *http.Request) {
197+
// Show headers for this route
198+
var requestHeaders []HeaderInfo
199+
for name, values := range r.Header {
200+
requestHeaders = append(requestHeaders, HeaderInfo{
201+
Name: name,
202+
Values: values,
203+
})
204+
}
205+
206+
response := HeaderResponse{
207+
Message: "Special route with different header middleware",
208+
RequestHeaders: requestHeaders,
209+
Timestamp: time.Now().Format(time.RFC3339),
210+
}
211+
212+
w.Header().Set("Content-Type", "application/json")
213+
jsonData, err := json.MarshalIndent(response, "", " ")
214+
if err != nil {
215+
http.Error(w, "Internal server error", http.StatusInternalServerError)
216+
return
217+
}
218+
_, err = fmt.Fprint(w, string(jsonData))
219+
if err != nil {
220+
http.Error(w, "Failed to write response", http.StatusInternalServerError)
221+
return
222+
}
223+
}
224+
225+
specialRoute, err := httpserver.NewRouteFromHandlerFunc(
226+
"special",
227+
"/special",
228+
specialHandler,
229+
specialMw...,
230+
)
231+
if err != nil {
232+
return nil, fmt.Errorf("failed to create special route: %w", err)
233+
}
234+
235+
return httpserver.Routes{*simpleRoute, *headerRoute, *specialRoute}, nil
236+
}
237+
238+
// createHTTPServer creates the HTTP server with configured routes
239+
func createHTTPServer(
240+
routes []httpserver.Route,
241+
logHandler slog.Handler,
242+
) (*httpserver.Runner, error) {
243+
config, err := httpserver.NewConfig(ListenOn, routes, httpserver.WithDrainTimeout(DrainTimeout))
244+
if err != nil {
245+
return nil, fmt.Errorf("failed to create HTTP server config: %w", err)
246+
}
247+
248+
return httpserver.NewRunner(
249+
httpserver.WithConfig(config),
250+
httpserver.WithLogHandler(logHandler),
251+
)
252+
}
253+
254+
// createSupervisor initializes go-supervisor with the HTTP server
255+
func createSupervisor(
256+
ctx context.Context,
257+
logHandler slog.Handler,
258+
runnable supervisor.Runnable,
259+
) (*supervisor.PIDZero, error) {
260+
sv, err := supervisor.New(
261+
supervisor.WithContext(ctx),
262+
supervisor.WithLogHandler(logHandler),
263+
supervisor.WithRunnables(runnable))
264+
if err != nil {
265+
return nil, fmt.Errorf("failed to create supervisor: %w", err)
266+
}
267+
268+
return sv, nil
269+
}
270+
271+
func main() {
272+
// Configure the logger
273+
handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
274+
Level: slog.LevelDebug,
275+
})
276+
slog.SetDefault(slog.New(handler))
277+
278+
// Create base context
279+
ctx := context.Background()
280+
281+
// Build routes
282+
routes, err := buildRoutes(handler)
283+
if err != nil {
284+
slog.Error("Failed to build routes", "error", err)
285+
os.Exit(1)
286+
}
287+
288+
// Create the HTTP server
289+
httpServer, err := createHTTPServer(routes, handler)
290+
if err != nil {
291+
slog.Error("Failed to create HTTP server", "error", err)
292+
os.Exit(1)
293+
}
294+
295+
// Create the supervisor
296+
sv, err := createSupervisor(ctx, handler, httpServer)
297+
if err != nil {
298+
slog.Error("Failed to setup server", "error", err)
299+
os.Exit(1)
300+
}
301+
302+
// Start the supervisor
303+
slog.Info("Starting headers middleware example server", "listen", ListenOn)
304+
slog.Info("Available endpoints:")
305+
slog.Info(" GET / - Simple response with header manipulation")
306+
slog.Info(" GET /headers - Inspect request headers (shows middleware effects)")
307+
slog.Info(" GET /special - Route with different header middleware")
308+
slog.Info("")
309+
slog.Info("Test with curl:")
310+
slog.Info(
311+
` curl -H "X-Forwarded-For: 192.168.1.1" -H "User-Agent: test" -v http://localhost:8082/headers`,
312+
)
313+
slog.Info(" Notice: X-Forwarded-For is removed, custom headers are added")
314+
315+
if err := sv.Run(); err != nil {
316+
slog.Error("Supervisor failed", "error", err)
317+
os.Exit(1)
318+
}
319+
}

0 commit comments

Comments
 (0)