Building a Flexible Middleware Chain in Go
In web development, middleware is a powerful pattern for handling cross-cutting concerns like authentication, logging, request validation, and more. This challenge will guide you in building a flexible and composable middleware chain mechanism in Go, allowing you to apply multiple operations to incoming requests before they reach your final handler.
Problem Description
You are tasked with creating a MiddlewareChain structure in Go that can hold a sequence of middleware functions. Each middleware function should accept a http.Handler and return a new http.Handler. This allows you to "wrap" existing handlers with new logic. The MiddlewareChain should provide a method to add middleware and a method to build the final chained handler.
Key Requirements:
- Middleware Signature: Define a
Middlewaretype that matches the signaturefunc(next http.Handler) http.Handler. MiddlewareChainStructure: Create aMiddlewareChainstruct to store a list ofMiddlewarefunctions.AddMethod: Implement anAdd(middleware Middleware)method onMiddlewareChainto append middleware functions to the chain.BuildMethod: Implement aBuild(finalHandler http.Handler)method that takes the ultimatehttp.Handler(your application's core logic) and applies all added middleware in sequence. The middleware should be applied in the order they were added.- Order of Execution: Ensure that the middleware is executed in the order they are added to the chain. The first middleware added will be the outermost, and the last middleware added will be the innermost (closest to the
finalHandler).
Expected Behavior:
When the Build method is called, it should return an http.Handler. When this returned handler is served, each middleware should have an opportunity to execute its logic. Specifically, the logic within a middleware before calling next(req, res) will execute first, followed by the next handler (which might be another middleware or the final handler), and then the logic after calling next(req, res).
Edge Cases:
- Empty Chain: If no middleware is added,
Buildshould simply return thefinalHandlerunchanged. - Single Middleware: The chain should correctly handle a single middleware function.
Examples
Example 1: Basic Logging and Authentication
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
)
// Assume Middleware and MiddlewareChain are defined as per requirements
// LoggerMiddleware logs the request method and URL.
func LoggerMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("-> Logging: %s %s\n", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
fmt.Printf("<- Logging: %s %s\n", r.Method, r.URL.Path)
})
}
// AuthMiddleware checks for a specific header.
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-Auth-Token") != "supersecret" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
fmt.Println("-> Auth OK")
next.ServeHTTP(w, r)
fmt.Println("<- Auth OK")
})
}
// FinalHandler is the core application logic.
func FinalHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println("Executing Final Handler")
w.WriteHeader(http.StatusOK)
w.Write([]byte("Hello, World!"))
}
func main() {
chain := NewMiddlewareChain() // Assuming a constructor function
chain.Add(LoggerMiddleware)
chain.Add(AuthMiddleware)
finalHTTPHandler := http.HandlerFunc(FinalHandler)
builtHandler := chain.Build(finalHTTPHandler)
// Simulate a request
req, err := http.NewRequest("GET", "/", nil)
if err != nil {
panic(err)
}
req.Header.Set("X-Auth-Token", "supersecret")
rr := httptest.NewRecorder()
builtHandler.ServeHTTP(rr, req)
fmt.Printf("Response Body: %s\n", rr.Body.String())
fmt.Printf("Response Status: %d\n", rr.Code)
}
Expected Output (from main execution):
-> Logging: GET /
-> Auth OK
Executing Final Handler
<- Auth OK
<- Logging: GET /
Response Body: Hello, World!
Response Status: 200
Explanation:
The LoggerMiddleware is added first, followed by AuthMiddleware. When the request is served:
LoggerMiddleware's "before" logic runs.AuthMiddleware's "before" logic runs.- The
FinalHandlerexecutes. AuthMiddleware's "after" logic runs.LoggerMiddleware's "after" logic runs.
Example 2: Unauthorized Request
Using the same middleware and MiddlewareChain as Example 1, but simulating a request without the correct token.
// ... (Previous definitions of LoggerMiddleware, AuthMiddleware, FinalHandler, NewMiddlewareChain, etc.)
func main() {
chain := NewMiddlewareChain()
chain.Add(LoggerMiddleware)
chain.Add(AuthMiddleware)
finalHTTPHandler := http.HandlerFunc(FinalHandler)
builtHandler := chain.Build(finalHTTPHandler)
// Simulate an unauthorized request
req, err := http.NewRequest("GET", "/", nil)
if err != nil {
panic(err)
}
// Missing X-Auth-Token header
rr := httptest.NewRecorder()
builtHandler.ServeHTTP(rr, req)
fmt.Printf("Response Body: %s\n", rr.Body.String())
fmt.Printf("Response Status: %d\n", rr.Code)
}
Expected Output (from main execution):
-> Logging: GET /
Response Body: Unauthorized
Response Status: 401
Explanation:
The LoggerMiddleware runs, then AuthMiddleware. When AuthMiddleware checks the header and finds it missing, it writes an error response and returns, preventing the FinalHandler from being called. The LoggerMiddleware's "after" logic would not run in this case because AuthMiddleware short-circuited the chain.
Example 3: Empty Chain
// ... (Previous definitions of FinalHandler, NewMiddlewareChain)
func main() {
chain := NewMiddlewareChain() // No middleware added
finalHTTPHandler := http.HandlerFunc(FinalHandler)
builtHandler := chain.Build(finalHTTPHandler)
// Simulate a request
req, err := http.NewRequest("GET", "/", nil)
if err != nil {
panic(err)
}
rr := httptest.NewRecorder()
builtHandler.ServeHTTP(rr, req)
fmt.Printf("Response Body: %s\n", rr.Body.String())
fmt.Printf("Response Status: %d\n", rr.Code)
}
Expected Output (from main execution):
Executing Final Handler
Response Body: Hello, World!
Response Status: 200
Explanation:
With an empty chain, the Build method returns the finalHandler directly. The request is served by the FinalHandler without any middleware interference.
Constraints
- The Go version used should be 1.13 or higher.
- No external libraries beyond the standard Go
net/httppackage are allowed for the coreMiddlewareChainimplementation. - The
Buildmethod should have a time complexity that is linear with respect to the number of middleware functions in the chain.
Notes
- Consider how you will iterate through the middleware and "wrap" them correctly. The order of application is crucial.
- Think about the recursive nature of applying middleware. Each middleware takes a handler (which could be the next middleware or the final handler) and returns a new handler.
- A common pattern for implementing the
Buildmethod is to start with thefinalHandlerand then iteratively apply each middleware from the end of the chain backwards to the beginning.