Hone logo
Hone
Problems

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:

  1. Middleware Signature: Define a Middleware type that matches the signature func(next http.Handler) http.Handler.
  2. MiddlewareChain Structure: Create a MiddlewareChain struct to store a list of Middleware functions.
  3. Add Method: Implement an Add(middleware Middleware) method on MiddlewareChain to append middleware functions to the chain.
  4. Build Method: Implement a Build(finalHandler http.Handler) method that takes the ultimate http.Handler (your application's core logic) and applies all added middleware in sequence. The middleware should be applied in the order they were added.
  5. 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, Build should simply return the finalHandler unchanged.
  • 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:

  1. LoggerMiddleware's "before" logic runs.
  2. AuthMiddleware's "before" logic runs.
  3. The FinalHandler executes.
  4. AuthMiddleware's "after" logic runs.
  5. 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/http package are allowed for the core MiddlewareChain implementation.
  • The Build method 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 Build method is to start with the finalHandler and then iteratively apply each middleware from the end of the chain backwards to the beginning.
Loading editor...
go