Skip to content

Commit 4df95b8

Browse files
authored
split out httpserver.middleware into a new sub-package, with improved API (#50)
* split out httpserver.middleware into a new sub-package, with improved API * add more tests * simplify and cleanup middleware implementations, and add/adjust examples * clean up docs * clean up examples * more docs cleanup * remove loop var capture * clean up wildcard, add docs and more tests * make the logger and recovery accept a slog.Handler implementation instead * add a compliance test, and rewrite the json enforcer example middleware * add note to header middleware about limitations * add some guardrails and notes to the example code * docs * simplify and fix the json middleware example * update doc * docs * rename * remove cruft * roll-back some of the API breaking changes to routes.go, add deprecation notices
1 parent 371ec15 commit 4df95b8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+4104
-1261
lines changed

examples/README.md

Lines changed: 13 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,21 @@
1-
# go-supervisor Examples
1+
# Examples
22

3-
This directory contains examples demonstrating how to use the go-supervisor package in real-world scenarios. Each example showcases different aspects of service lifecycle management, configuration, and state handling.
3+
Several working examples of go-supervisor usage.
44

5-
> **Note:** More examples are coming soon as we add additional runnable implementations to the project. Currently, the HTTP server example demonstrates a complete implementation of the supervisor interfaces.
5+
## [http](./http/)
6+
Basic HTTP server with graceful shutdown and configuration reloading.
67

7-
## HTTP Server Example
8+
## [custom_middleware](./custom_middleware/)
9+
HTTP server with middleware that transforms responses.
810

9-
The [http](./http/) directory contains a complete example of an HTTP server managed by the go-supervisor package.
11+
## [composite](./composite/)
12+
Multiple dynamic services managed as a single unit, using Generics.
1013

11-
This example demonstrates:
12-
- Creating a `runnables.HttpServer`, implementing `supervisor.PIDZero` interfaces (Runnable, Reloadable, Stateable)
13-
- Configuration of routes and handlers, from the `httpserver` package
14-
- Logging with an `slog.Handler`
14+
## [httpcluster](./httpcluster/)
15+
Similar to composite, but designed specifically for running several `httpserver` instances, with a channel-based config "siphon" for dynamic updates.
1516

16-
Key components of the HTTP server example:
17-
18-
1. **Route Configuration**: Shows how to create standard and wildcard routes
19-
2. **Configuration Callback**: Demonstrates dynamic configuration loading
20-
3. **State Management**: Uses FSM-based state tracking for the server lifecycle
21-
4. **Supervisor Integration**: Properly initializes and uses the PIDZero supervisor
22-
23-
To run the HTTP server example:
17+
## Running
2418

2519
```bash
26-
cd examples/http
27-
go build
28-
./http
29-
```
30-
31-
Then access the server at:
32-
- `http://localhost:8080/` - Index route
33-
- `http://localhost:8080/status` - Status endpoint
34-
- `http://localhost:8080/api/anything` - Wildcard API route
35-
36-
Press Ctrl+C to trigger a graceful shutdown.
37-
38-
## Implementing Your Own Runnable
39-
For detailed instructions on implementing your own runnable, please refer to the root-level README. The HTTP example in this directory provides a complete implementation that you can use as a reference.
40-
41-
See the [HTTP example](./http/main.go) for a complete implementation.
20+
go run ./examples/<name>
21+
```

examples/custom_middleware/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# ignore the compiled binary
2+
custom_middleware

examples/custom_middleware/README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Custom Middleware Example
2+
3+
This example demonstrates how to create custom middleware for the httpserver package.
4+
5+
## What It Shows
6+
7+
- Creating a custom middleware that transforms HTTP responses
8+
- Using built-in middleware from the httpserver package
9+
- Correct middleware ordering and composition
10+
- Separation of concerns between middleware layers
11+
12+
## Key Components
13+
14+
### JSON Enforcer Middleware
15+
A custom middleware that ensures all responses are JSON formatted. Non-JSON responses are wrapped in `{"response": "content"}` while valid JSON passes through unchanged.
16+
17+
### Headers Middleware
18+
Uses the built-in headers middleware to set Content-Type, CORS, and security headers.
19+
20+
## Running the Example
21+
22+
```bash
23+
go run ./examples/custom_middleware
24+
```
25+
26+
The server starts on `:8081` with several endpoints to demonstrate the middleware behavior.
27+
28+
## Endpoints
29+
30+
- `GET /` - Returns plain text (wrapped in JSON)
31+
- `GET /api/data` - Returns JSON (preserved as-is)
32+
- `GET /html` - Returns HTML (wrapped in JSON)
33+
- `GET /error` - Returns 404 error (wrapped in JSON)
34+
- `GET /panic` - Triggers panic recovery middleware
35+
36+
## Middleware Ordering
37+
38+
The example demonstrates why middleware order matters:
39+
40+
1. **Recovery** - Must be first to catch panics
41+
2. **Security** - Set security headers early
42+
3. **Logging** - Log all requests
43+
4. **Metrics** - Collect request metrics
44+
5. **Headers** - Set response headers before handler
45+
46+
See the code comments in `main.go`.
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package example
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"net/http"
7+
8+
"github.com/robbyt/go-supervisor/runnables/httpserver"
9+
)
10+
11+
// ResponseBuffer captures response data for transformation
12+
//
13+
// This is a simple example for demonstration purposes and is not intended for
14+
// production use. Limitations:
15+
// - Does not preserve optional HTTP interfaces (http.Hijacker, http.Flusher, http.Pusher)
16+
// - Not safe for concurrent writes from multiple goroutines within the same request
17+
// - No memory limits on buffered content
18+
//
19+
// Each request gets its own ResponseBuffer instance, so different requests won't
20+
// interfere with each other.
21+
type ResponseBuffer struct {
22+
buffer *bytes.Buffer
23+
headers http.Header
24+
status int
25+
}
26+
27+
// NewResponseBuffer creates a new response buffer
28+
func NewResponseBuffer() *ResponseBuffer {
29+
return &ResponseBuffer{
30+
buffer: new(bytes.Buffer),
31+
headers: make(http.Header),
32+
status: 0, // 0 means not set yet
33+
}
34+
}
35+
36+
// Header implements http.ResponseWriter
37+
func (rb *ResponseBuffer) Header() http.Header {
38+
return rb.headers
39+
}
40+
41+
// Write implements http.ResponseWriter
42+
func (rb *ResponseBuffer) Write(data []byte) (int, error) {
43+
return rb.buffer.Write(data)
44+
}
45+
46+
// WriteHeader implements http.ResponseWriter
47+
func (rb *ResponseBuffer) WriteHeader(statusCode int) {
48+
if rb.status == 0 {
49+
rb.status = statusCode
50+
}
51+
}
52+
53+
// Status implements httpserver.ResponseWriter
54+
func (rb *ResponseBuffer) Status() int {
55+
if rb.status == 0 && rb.buffer.Len() > 0 {
56+
return http.StatusOK
57+
}
58+
return rb.status
59+
}
60+
61+
// Written implements httpserver.ResponseWriter
62+
func (rb *ResponseBuffer) Written() bool {
63+
return rb.buffer.Len() > 0 || rb.status != 0
64+
}
65+
66+
// Size implements httpserver.ResponseWriter
67+
func (rb *ResponseBuffer) Size() int {
68+
return rb.buffer.Len()
69+
}
70+
71+
// transformToJSON wraps non-JSON content in a JSON response
72+
func transformToJSON(data []byte) ([]byte, error) {
73+
// Use json.Valid for efficient validation without unmarshaling
74+
if json.Valid(data) {
75+
return data, nil // Valid JSON, return as-is
76+
}
77+
78+
// If not valid JSON, wrap it
79+
response := map[string]string{
80+
"response": string(data),
81+
}
82+
83+
return json.Marshal(response)
84+
}
85+
86+
// New creates a middleware that transforms all responses to JSON format.
87+
// Non-JSON responses are wrapped in {"response": "content"}.
88+
// Valid JSON responses are preserved as-is.
89+
func New() httpserver.HandlerFunc {
90+
return func(rp *httpserver.RequestProcessor) {
91+
// Store original writer before buffering
92+
originalWriter := rp.Writer()
93+
94+
// Buffer the response to capture output
95+
buffer := NewResponseBuffer()
96+
rp.SetWriter(buffer)
97+
98+
// Continue to next middleware/handler
99+
rp.Next()
100+
101+
// RESPONSE PHASE: Transform response to JSON
102+
originalData := buffer.buffer.Bytes()
103+
statusCode := buffer.Status()
104+
if statusCode == 0 {
105+
statusCode = http.StatusOK
106+
}
107+
108+
// Copy headers to original writer
109+
for key, values := range buffer.Header() {
110+
for _, value := range values {
111+
originalWriter.Header().Add(key, value)
112+
}
113+
}
114+
115+
// Check if this status code should have no body per HTTP spec
116+
// 204 No Content and 304 Not Modified MUST NOT have a message body
117+
if statusCode == http.StatusNoContent || statusCode == http.StatusNotModified {
118+
originalWriter.WriteHeader(statusCode)
119+
return
120+
}
121+
122+
// Transform captured data to JSON
123+
if len(originalData) == 0 && buffer.status == 0 {
124+
return
125+
}
126+
127+
// Transform to JSON if needed
128+
jsonData, err := transformToJSON(originalData)
129+
if err != nil {
130+
// Fallback: wrap error in JSON
131+
jsonData = []byte(`{"error":"Unable to encode response"}`)
132+
}
133+
134+
// Ensure JSON content type
135+
originalWriter.Header().Set("Content-Type", "application/json")
136+
137+
// Write status and transformed data
138+
originalWriter.WriteHeader(statusCode)
139+
if _, err := originalWriter.Write(jsonData); err != nil {
140+
// Response is already committed, cannot recover from write error
141+
return
142+
}
143+
}
144+
}

0 commit comments

Comments
 (0)