Hone logo
Hone
Problems

Go Middleware Chain Implementation

You will implement a flexible middleware chain pattern in Go. This pattern is crucial for building robust web applications and APIs, allowing you to process requests and responses in a layered, composable manner. You'll create a system where multiple middleware functions can be executed sequentially, each having the opportunity to modify the request, the response, or control the flow of execution.

Problem Description

Your task is to design and implement a middleware chain that can process a "context" object. A middleware function will be a function that accepts a context and a "next" middleware function. It should be able to perform some operation, then call the "next" middleware, or choose not to call it, effectively terminating the chain.

Key Requirements:

  1. Middleware Signature: Define a Middleware type that represents a middleware function. This function should take a context.Context and a http.HandlerFunc (or a similar interface representing the next handler/middleware) as input and return a new http.HandlerFunc. The http.HandlerFunc returned by a middleware is the "next" handler to be executed in the chain.

  2. Chain Builder: Implement a Chain function that accepts a variadic list of Middleware functions and returns a single http.HandlerFunc. This Chain function should construct the middleware chain such that when the returned http.HandlerFunc is invoked, the middleware functions are executed in the order they were provided.

  3. Context Propagation: Ensure that the context.Context is correctly passed down the chain.

  4. Control Flow: Middleware should have the ability to stop the chain's execution by not calling the next handler.

  5. Final Handler: The chain should eventually execute a final http.HandlerFunc (the actual request handler).

Expected Behavior:

When the Chain function is used to build a handler and that handler is invoked:

  • The first middleware in the provided list will be executed.
  • If the first middleware calls next, the second middleware will be executed, and so on.
  • The last middleware in the list will call the final, underlying http.HandlerFunc.
  • If any middleware decides not to call next, the subsequent middleware and the final handler will not be executed.

Edge Cases:

  • An empty chain (no middleware provided).
  • A chain with only one middleware.
  • Middleware that panics.

Examples

Example 1: Basic Logging Middleware

Consider a scenario where you want to log the start and end of a request processing.

Middleware Definition:

// Assuming a simplified HandlerFunc signature for illustration
type HandlerFunc func(ctx context.Context) error

type Middleware func(next HandlerFunc) HandlerFunc

func LoggingMiddleware() Middleware {
    return func(next HandlerFunc) HandlerFunc {
        return func(ctx context.Context) error {
            fmt.Println("Entering request")
            err := next(ctx) // Call the next middleware/handler
            if err != nil {
                fmt.Printf("Error during request: %v\n", err)
            }
            fmt.Println("Exiting request")
            return err
        }
    }
}

func MyHandler() HandlerFunc {
    return func(ctx context.Context) error {
        fmt.Println("Executing main handler")
        return nil
    }
}

// Usage:
// handler := Chain(LoggingMiddleware())(MyHandler())
// handler(context.Background())

Input:

context.Background() (representing the start of a request)

Output:

Entering request
Executing main handler
Exiting request

Explanation:

The LoggingMiddleware wraps MyHandler. When the chained handler is called, LoggingMiddleware executes its "entering" logic, then calls next (which is MyHandler). MyHandler executes its logic, returns nil, and control returns to LoggingMiddleware, which then executes its "exiting" logic.

Example 2: Authentication Middleware (Stopping the Chain)

Imagine an authentication middleware that checks for a valid token. If the token is invalid, it should stop further processing.

Middleware Definition:

type HandlerFunc func(ctx context.Context) error

type Middleware func(next HandlerFunc) HandlerFunc

func AuthMiddleware() Middleware {
    return func(next HandlerFunc) HandlerFunc {
        return func(ctx context.Context) error {
            fmt.Println("Authenticating...")
            // Simulate token check
            isAuthenticated := false // Change to true to allow chain to proceed
            if !isAuthenticated {
                fmt.Println("Authentication failed. Aborting.")
                return fmt.Errorf("unauthenticated")
            }
            fmt.Println("Authentication successful.")
            return next(ctx) // Only call next if authenticated
        }
    }
}

func ProtectedHandler() HandlerFunc {
    return func(ctx context.Context) error {
        fmt.Println("Accessing protected resource.")
        return nil
    }
}

// Usage:
// handler := Chain(AuthMiddleware())(ProtectedHandler())
// handler(context.Background())

Input:

context.Background()

Output (when isAuthenticated is false):

Authenticating...
Authentication failed. Aborting.

Explanation:

The AuthMiddleware checks for authentication. Since isAuthenticated is false, it prints an error and returns an error without calling next. Consequently, ProtectedHandler is never executed.

Example 3: Chaining Multiple Middleware

// Assume HandlerFunc and Middleware types as defined in Example 1

func AddHeaderMiddleware(key, value string) Middleware {
    return func(next HandlerFunc) HandlerFunc {
        return func(ctx context.Context) error {
            fmt.Printf("Adding header: %s=%s\n", key, value)
            // In a real HTTP scenario, you'd modify a request object here.
            // For this simplified context, we just simulate the action.
            return next(ctx)
        }
    }
}

func MyHandlerWithHeaders() HandlerFunc {
    return func(ctx context.Context) error {
        fmt.Println("Executing handler that assumes headers are present.")
        return nil
    }
}

// Usage:
// handler := Chain(
//     LoggingMiddleware(),
//     AddHeaderMiddleware("Content-Type", "application/json"),
//     AddHeaderMiddleware("X-API-Key", "abcdef123"),
// )(MyHandlerWithHeaders())
// handler(context.Background())

Input:

context.Background()

Output:

Entering request
Adding header: Content-Type=application/json
Adding header: X-API-Key=abcdef123
Executing handler that assumes headers are present.
Exiting request

Explanation:

The middleware are chained, and Chain ensures they are executed in order. LoggingMiddleware is first, then the two AddHeaderMiddleware instances, and finally MyHandlerWithHeaders. The LoggingMiddleware's exit logic executes after the entire chain completes.

Constraints

  • The Middleware type must have a signature compatible with wrapping http.HandlerFunc or a similar callable that takes a context.Context.
  • The Chain function must handle an arbitrary number of middleware functions.
  • The solution should be efficient and avoid unnecessary overhead.
  • The final handler must be callable with a context.Context.
  • Your implementation should gracefully handle panics within middleware if possible, or at least not break the chain unexpectedly in a way that prevents the Chain function from returning a valid handler. (Consider how you might recover from panics).

Notes

  • Think about how to construct the chain in reverse. The last middleware needs to wrap the actual handler, the second-to-last needs to wrap the last middleware, and so on.
  • Consider using context.WithValue if your middleware needs to pass specific data down the chain, though the primary focus is on control flow and general context propagation.
  • For a real-world HTTP server, the http.HandlerFunc signature is func(w http.ResponseWriter, r *http.Request). Your Middleware would likely wrap this signature, and the Chain function would return an http.HandlerFunc. For this exercise, you can simplify the handler signature to func(ctx context.Context) error if it makes the core logic clearer. However, if you choose to use the http.HandlerFunc signature, ensure your middleware correctly handles http.ResponseWriter and *http.Request.
  • A common pattern for middleware is to return a new http.HandlerFunc that incorporates the original handler.
Loading editor...
go