middlewares-go-net-http

Middlewares in Go with net/http for logging and auth

  • 3 min

A middleware in Go is a function that wraps an HTTP handler to add common behavior before or after handling a request.

The request passes through several layers before reaching the final handler. Each layer can measure time, validate a token, write logs, or cut off the request if something is wrong.

The good news is that in Go we don’t need anything special. With net/http, a middleware is simply a function that receives and returns an http.Handler.

Recalling handlers

An HTTP handler in Go implements this interface:

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}
Copied!

And a regular function can become a handler with http.HandlerFunc.

func hello(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello"))
}

handler := http.HandlerFunc(hello)
Copied!

Structure of a middleware

The typical approach is this:

func middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Before the handler

        next.ServeHTTP(w, r)

        // After the handler
    })
}
Copied!

next represents the next step in the chain. It can be another middleware or the final handler.

Logging middleware

Let’s look at a practical example: logging method, route, and duration.

package main

import (
    "log"
    "net/http"
    "time"
)

func logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()

        defer func() {
            log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
        }()

        next.ServeHTTP(w, r)
    })
}
Copied!

And we use it like this:

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello"))
    })

    server := logging(mux)

    log.Fatal(http.ListenAndServe(":8080", server))
}
Copied!

All routes in the mux now go through the middleware.

Authentication middleware

A middleware can also cut off the request before reaching the handler.

func auth(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")

        if token != "Bearer secret" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }

        next.ServeHTTP(w, r)
    })
}
Copied!

Notice the return. If the token is invalid, we respond with the error and do not call next.

This token example is only for educational purposes. In a real application we would validate JWT, sessions, API keys, or the appropriate mechanism, and we would never hardcode secrets.

Chaining middlewares

We can wrap multiple layers:

server := logging(auth(mux))
Copied!

The request will first pass through logging, then auth, and finally mux.

If you want it to read better, you can create a small chain function.

func chain(h http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        h = middlewares[i](h)
    }

    return h
}
Copied!

Usage:

server := chain(mux, logging, auth)
Copied!

Order matters. Logging all requests is not the same as logging only authorized ones.

Recovery middleware

The net/http server already recovers from handler panics, logs a trace, and cuts off the response or connection. Having your own middleware allows you to control the logging and attempt to send a 500 response.

import "runtime/debug"

func recoverMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("panic handling %s: %v\n%s", r.URL.Path, err, debug.Stack())
                http.Error(w, "Internal error", http.StatusInternalServerError)
            }
        }()

        next.ServeHTTP(w, r)
    })
}
Copied!

This does not turn panic into a normal error handling tool. If the handler has already sent headers, we won’t be able to change the status code to 500. Furthermore, recovering from a panic does not fix the bug: we must log it and correct its cause.