golib/http/middleware/README.md

146 lines
3.6 KiB
Markdown

# http/middleware
[![Go Reference](https://pkg.go.dev/badge/github.com/therootcompany/golib/http/middleware.svg)](https://pkg.go.dev/github.com/therootcompany/golib/http/middleware)
A simple zero-cost middleware handler for Go's native `net/http` `ServeMux`. \
(only 50 lines long, using only `net/http` and `slices`)
Turns tedious this:
```go
mux.Handle("GET /api/version", logRequests(timeRequests(http.HandlerFunc(getVersion))))
mux.Handle("GET /api/data", logRequests(timeRequests(requireAuth(requireAdmin(http.HandlerFunc(getData))))))
```
Into organizable this:
```go
mw := middleware.WithMux(mux, logRequests, timeRequests)
mw.HandleFunc("GET /api/version", getVersion)
authMW := mw.With(requireAuth, requireAdmin)
authMW.HandleFunc("GET /api/data", getData)
```
Using stdlib this:
```go
type Middleware func(http.Handler) http.Handler
```
**Zero-cost** because each invocation of `mv.Handle(handler)` composes the function calls _exactly_ the same way as when done manually. \
(the setup is done during route initialization and has no additional impact on requests)
## Usage
- Create with `middleware.New(middlewares...)`
- Extend with `mw.Use(midlewares...)` (copies and appends)
- Apply with `specific.Handle(handler)`
```go
package main
import (
"net/http"
"github.com/therootcompany/golib/middleware"
)
func main() {
mux := http.NewServeMux()
mw := middleware.WithMux(mux, recoverPanics, logRequests, basicAuth)
mux.HandleFunc("GET /api/data", getData)
mux.HandleFunc("POST /api/data", postData)
adminMW := mw.Use(requireAdmin)
adminMW.HandleFunc("DELETE /api/data", deleteData)
http.ListenAndServe(":8080", mux)
}
```
### Example Middleware
Middleware is any function that wraps and returns the built-in `http.Handler` handler type.
```go
type Middleware func(http.Handler) http.Handler
```
#### Example: Panic handler
```go
func recoverPanics(next http.Handler) http.Handler {
return func(w http.ResponseWriter, r *http.Request) {
defer func() {
if _err := recover(); _err != nil {
err, ok := _err.(error)
if !ok {
err = fmt.Errorf("%v", _err)
}
api.InternalError(w, r, err)
return
}
}()
next.ServeHTTP(w, r)
}
}
```
#### Example: Request logger
```go
func logRequests(next http.Handler) http.Handler {
return func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
}
}
```
#### Example: Basic Auth Verifier, using [`envauth`](https://github.com/therootcompany/golib/tree/main/auth/envauth)
```go
import "github.com/therootcompany/golib/auth/envauth"
var creds = envauth.BasicCredentials{
Username: os.Getenv("BASIC_AUTH_USERNAME"),
Password: os.Getenv("BASIC_AUTH_PASSWORD"),
}
func basicAuth(next http.Handler) http.Handler {
return func(w http.ResponseWriter, r *http.Request) {
user, pass, _ := r.BasicAuth()
if err := creds.Verify(user, pass); err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
}
}
```
#### Example: Admin role checker
```go
func requireAdmin(next http.Handler) http.Handler {
return func(w http.ResponseWriter, r *http.Request) {
// Assume JWT in context with roles
jws := r.Context().Value("jwt").(jwt.JWS)
if !slices.Contains(jws.Claims.Roles, "admin") {
http.Error(w, "Forbidden: Admin access required", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
}
}
```