middleware

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 26, 2025 License: EUPL-1.2 Imports: 11 Imported by: 0

Documentation

Overview

Package middleware contains simple middleware implementations and utilities. The middlewares in this package do not depend on functionality from httputil/mux, and can be used with any router that uses middleware, or with net/http directly.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func AllowAll

func AllowAll() []string

AllowAll returns the CORS and Permissions-Policy wildcard pattern.

Example (Preflight)
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	// Allow all values. This creates an open API.
	// Note that this is only allowed when AllowCredentials is false.
	cors := middleware.CORS{
		AllowCredentials: false,
		AllowHeaders:     middleware.AllowAll(),
		AllowMethods:     middleware.AllowAll(),
		AllowOrigins:     middleware.AllowAll(),
		ExposeHeaders:    middleware.AllowAll(),
	}

	router := mux.NewRouter()
	router.HandleFunc("/", func(http.ResponseWriter, *http.Request) {})
	router.Use(cors.Handler)

	request := httptest.NewRequest(http.MethodOptions, "/", nil)
	request.Header.Set("Origin", "http://example.invalid")
	request.Header.Set("Access-Control-Request-Method", http.MethodGet)
	request.Header.Set("Access-Control-Request-Headers", "X-API-Key")

	rr := httptest.NewRecorder()
	router.ServeHTTP(rr, request)

	printHeader := func(name string) {
		fmt.Printf("%s: %v\n", name, rr.Header().Values(name))
	}

	fmt.Printf("Status: %d\n", rr.Result().StatusCode)
	printHeader("Access-Control-Allow-Credentials")
	printHeader("Access-Control-Allow-Headers")
	printHeader("Access-Control-Allow-Methods")
	printHeader("Access-Control-Allow-Origin")
	printHeader("Access-Control-Expose-Headers")
	printHeader("Access-Control-Max-Age")
}
Output:

Status: 204
Access-Control-Allow-Credentials: []
Access-Control-Allow-Headers: [*]
Access-Control-Allow-Methods: [*]
Access-Control-Allow-Origin: [*]
Access-Control-Expose-Headers: []
Access-Control-Max-Age: []
Example (Simple)
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	// Allow all values. This creates an open API.
	// Note that this is only allowed when AllowCredentials is false.
	cors := middleware.CORS{
		AllowCredentials: false,
		AllowHeaders:     middleware.AllowAll(),
		AllowMethods:     middleware.AllowAll(),
		AllowOrigins:     middleware.AllowAll(),
		ExposeHeaders:    middleware.AllowAll(),
	}

	router := mux.NewRouter()
	router.HandleFunc("/", func(http.ResponseWriter, *http.Request) {})
	router.Use(cors.Handler)

	request := httptest.NewRequest(http.MethodGet, "/", nil)
	request.Header.Set("Origin", "http://example.invalid")

	rr := httptest.NewRecorder()
	router.ServeHTTP(rr, request)

	printHeader := func(name string) {
		fmt.Printf("%s: %v\n", name, rr.Header().Values(name))
	}

	fmt.Printf("Status: %d\n", rr.Result().StatusCode)
	printHeader("Access-Control-Allow-Credentials")
	printHeader("Access-Control-Allow-Headers")
	printHeader("Access-Control-Allow-Methods")
	printHeader("Access-Control-Allow-Origin")
	printHeader("Access-Control-Expose-Headers")
	printHeader("Access-Control-Max-Age")
}
Output:

Status: 200
Access-Control-Allow-Credentials: []
Access-Control-Allow-Headers: [*]
Access-Control-Allow-Methods: [*]
Access-Control-Allow-Origin: [*]
Access-Control-Expose-Headers: [*]
Access-Control-Max-Age: []

func AllowMethodsFromPattern

func AllowMethodsFromPattern(request *http.Request) []string

AllowMethodsFromPattern returns the method from the request pattern. It returns '*' (allowing all methods) if the pattern contains no methods. Note that this only works with 'simple' CORS request (not preflight) and only if there is always a single method associated with a request path.

Example
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	router := mux.NewRouter()
	router.HandleFunc("GET /index.json", func(http.ResponseWriter, *http.Request) {})

	cors := middleware.CORS{
		AllowMethodsFunc: middleware.AllowMethodsFromPattern,
		AllowOrigins:     []string{"*"},
		AllowHeaders:     []string{"*"},
	}
	router.Use(cors.Handler)

	request := httptest.NewRequest(http.MethodGet, "/index.json", nil)
	request.Header.Set("Origin", "http://example.invalid")

	rr := httptest.NewRecorder()
	router.ServeHTTP(rr, request)

	printHeader := func(name string) {
		fmt.Printf("%s: %v\n", name, rr.Header().Values(name))
	}

	fmt.Printf("Status: %d\n", rr.Result().StatusCode)
	printHeader("Access-Control-Allow-Credentials")
	printHeader("Access-Control-Allow-Headers")
	printHeader("Access-Control-Allow-Methods")
	printHeader("Access-Control-Allow-Origin")
	printHeader("Access-Control-Expose-Headers")
	printHeader("Access-Control-Max-Age")
}
Output:

Status: 200
Access-Control-Allow-Credentials: []
Access-Control-Allow-Headers: [*]
Access-Control-Allow-Methods: [GET]
Access-Control-Allow-Origin: [*]
Access-Control-Expose-Headers: []
Access-Control-Max-Age: []
Example (NoMethods)
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	router := mux.NewRouter()
	router.HandleFunc("/index.json", func(http.ResponseWriter, *http.Request) {})

	cors := middleware.CORS{
		AllowMethodsFunc: middleware.AllowMethodsFromPattern,
		AllowOrigins:     []string{"*"},
		AllowHeaders:     []string{"*"},
	}
	router.Use(cors.Handler)

	request := httptest.NewRequest(http.MethodOptions, "/index.json", nil)
	request.Header.Set("Origin", "http://example.invalid")
	request.Header.Set("Access-Control-Request-Method", http.MethodGet)

	rr := httptest.NewRecorder()
	router.ServeHTTP(rr, request)

	printHeader := func(name string) {
		fmt.Printf("%s: %v\n", name, rr.Header().Values(name))
	}

	fmt.Printf("Status: %d\n", rr.Result().StatusCode)
	printHeader("Access-Control-Allow-Credentials")
	printHeader("Access-Control-Allow-Headers")
	printHeader("Access-Control-Allow-Methods")
	printHeader("Access-Control-Allow-Origin")
	printHeader("Access-Control-Expose-Headers")
	printHeader("Access-Control-Max-Age")
}
Output:

Status: 204
Access-Control-Allow-Credentials: []
Access-Control-Allow-Headers: [*]
Access-Control-Allow-Methods: [*]
Access-Control-Allow-Origin: [*]
Access-Control-Expose-Headers: []
Access-Control-Max-Age: []

func As

func As[T http.Handler](h http.Handler) (v T, ok bool)

As unwraps the given handler using Unwrap until it finds a handler of the given type. It returns the found handler and a boolean indicating success.

Example
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	// We want to find this handler in a situation with a lot of middleware.
	myHandler := http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
		fmt.Println("This is my handler!")
	})

	router := mux.NewRouter()
	router.Use(
		(&middleware.Log{}).Handler,
		(&middleware.CORS{}).Handler,
		(&middleware.SecurityHeaders{}).Handler,
		middleware.MaxBytes(0),
		middleware.Timeout(0, ""),
	)
	router.Handle("GET /something", myHandler)

	handler, _ := router.Handler(httptest.NewRequest(http.MethodGet, "/something", nil))

	found, ok := middleware.As[http.HandlerFunc](handler)
	if !ok {
		fmt.Println("handler not found")
		return
	}

	found.ServeHTTP(nil, nil)
}
Output:

This is my handler!
Example (Failed)
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	router := mux.NewRouter()
	router.Use(
		(&middleware.Log{}).Handler,
		(&middleware.CORS{}).Handler,
	)
	router.Subrouter("GET /something")

	handler, _ := router.Handler(httptest.NewRequest(http.MethodGet, "/something", nil))

	// We're not going to find an http.HandlerFunc
	_, ok := middleware.As[http.HandlerFunc](handler)
	if ok {
		fmt.Println("found handler")
	} else {
		fmt.Println("handler not found")
	}
}
Output:

handler not found

func Unwrap

func Unwrap(h http.Handler) http.Handler

Unwrap unwraps a middleware handler and returns the inner handler. It returns 'nil' if it cannot unwrap the given handler. In order to unwrap a middleware, it needs to implement the following method:

Unwrap() http.Handler

All middleware from this package supports unwrapping.

func Wrap

func Wrap(handler http.Handler, middleware ...Func) http.Handler

Wrap wraps the given handler with the given middleware functions. Middlewares will handle requests in order, meaning that the first middleware will be the first to receive a request.

Example
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"go.hofstra.dev/httputil/middleware"
)

func main() {
	handler := http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
		fmt.Println("Handler")
	})
	mw1 := func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			fmt.Println("Middleware 1")
			next.ServeHTTP(w, r)
		})
	}
	mw2 := func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			fmt.Println("Middleware 2")
			next.ServeHTTP(w, r)
		})
	}

	wrapped := middleware.Wrap(handler, mw1, mw2)
	wrapped.ServeHTTP(httptest.NewRecorder(),
		httptest.NewRequest(http.MethodGet, "/", nil))

}
Output:

Middleware 1
Middleware 2
Handler

Types

type CORS

type CORS struct {
	// AllowCredentials indicates that requests can be made using credentials.
	// This is omitted when false (the default).
	// Note that wildcard values are not allowed when this is set to true.
	AllowCredentials bool

	// AllowMethods indicates that the returned methods are allowed for requests to a path.
	// This defaults to allowing no headers.
	AllowHeaders []string

	// AllowHeadersFunc is an alternative to AllowHeaders.
	// It can be used to set the allowed methods based on the request.
	AllowHeadersFunc func(*http.Request) []string

	// AllowMethods indicates that the returned methods are allowed for requests to a path.
	// This defaults to allowing no methods.
	AllowMethods []string

	// AllowMethodsFunc is an alternative to AllowMethods.
	// It can be used to set the allowed methods based on routing data.
	// See `AllowMethodsFromPattern` to allow methods indicated in the routed pattern.
	AllowMethodsFunc func(*http.Request) []string

	// AllowOrigins indicates that the returned origins (hosts) are allowed for requests to a path.
	// This defaults to allowing no origins.
	AllowOrigins []string

	// AllowOriginsFunc is an alternative to AllowOrigins.
	// It can be used to set the allowed origins based on routing data.
	// Note that 'nil' is equivalent to '*', use []string{} to disallow all headers.
	AllowOriginsFunc func(*http.Request) []string

	// ExposeHeaders allows headers with the given names to be accessed by JavaScript.
	// This is omitted when nil (the default).
	ExposeHeaders []string

	// ExposeHeadersFunc is an alternative to ExposeHeaders.
	// It is omitted when 'nil'.
	ExposeHeadersFunc func(*http.Request) []string

	// MaxAge indicates the maximum duration that the CORS request can be cached.
	// This is omitted by default, in which case browsers default to 5s.
	// Browsers limit this value to 24 hours at most.
	MaxAge time.Duration

	// PassOptions makes the middleware pass options requests through to the next handler
	// instead of returning a 204 for the request.
	PassOptions bool
}

CORS implements a middleware for setting the Cross-Origin Resource Sharing headers. The default behaviour is to allow no CORS requests. Configure AllowOrigins and AllowMethods (or their corresponding functions) to allow CORS requests and configure AllowHeaders if additional headers are allowed in requests.

See the MDN documentation for details.

Example (Defaults)
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	cors := middleware.CORS{}

	router := mux.NewRouter()
	router.HandleFunc("/", func(http.ResponseWriter, *http.Request) {})
	router.Use(cors.Handler)

	request := httptest.NewRequest(http.MethodOptions, "/", nil)
	request.Header.Set("Origin", "http://example.invalid")
	request.Header.Set("Access-Control-Request-Method", http.MethodGet)
	request.Header.Set("Access-Control-Request-Headers", "X-API-Key,X-Access-Token")

	rr := httptest.NewRecorder()
	router.ServeHTTP(rr, request)

	printHeader := func(name string) {
		fmt.Printf("%s: %v\n", name, rr.Header().Values(name))
	}

	fmt.Printf("Status: %d\n", rr.Result().StatusCode)
	printHeader("Access-Control-Allow-Credentials")
	printHeader("Access-Control-Allow-Headers")
	printHeader("Access-Control-Allow-Methods")
	printHeader("Access-Control-Allow-Origin")
	printHeader("Access-Control-Expose-Headers")
	printHeader("Access-Control-Max-Age")
}
Output:

Status: 403
Access-Control-Allow-Credentials: []
Access-Control-Allow-Headers: []
Access-Control-Allow-Methods: []
Access-Control-Allow-Origin: []
Access-Control-Expose-Headers: []
Access-Control-Max-Age: []
Example (HeadersNotAllowed)
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"time"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	cors := middleware.CORS{
		AllowCredentials: true,
		AllowHeaders:     []string{"X-API-Key"},
		AllowMethods:     []string{http.MethodGet},
		AllowOrigins:     []string{"http://example.invalid"},
		ExposeHeaders:    []string{"MY-HEADER"},
		MaxAge:           90 * time.Second,
	}

	router := mux.NewRouter()
	router.HandleFunc("/", func(http.ResponseWriter, *http.Request) {})
	router.Use(cors.Handler)

	request := httptest.NewRequest(http.MethodOptions, "/", nil)
	request.Header.Set("Origin", "http://example.invalid")
	request.Header.Set("Access-Control-Request-Method", http.MethodGet)
	request.Header.Set("Access-Control-Request-Headers", "X-API-Key,X-Access-Token")

	rr := httptest.NewRecorder()
	router.ServeHTTP(rr, request)

	printHeader := func(name string) {
		fmt.Printf("%s: %v\n", name, rr.Header().Values(name))
	}

	fmt.Printf("Status: %d\n", rr.Result().StatusCode)
	printHeader("Access-Control-Allow-Credentials")
	printHeader("Access-Control-Allow-Headers")
	printHeader("Access-Control-Allow-Methods")
	printHeader("Access-Control-Allow-Origin")
	printHeader("Access-Control-Expose-Headers")
	printHeader("Access-Control-Max-Age")
}
Output:

Status: 406
Access-Control-Allow-Credentials: []
Access-Control-Allow-Headers: []
Access-Control-Allow-Methods: [GET]
Access-Control-Allow-Origin: [http://example.invalid]
Access-Control-Expose-Headers: []
Access-Control-Max-Age: []
Example (MethodNotAllowed)
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"time"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	cors := middleware.CORS{
		AllowCredentials: true,
		AllowHeaders:     []string{"X-API-Key"},
		AllowMethods:     []string{http.MethodGet},
		AllowOrigins:     []string{"http://example.invalid"},
		ExposeHeaders:    []string{"MY-HEADER"},
		MaxAge:           90 * time.Second,
	}

	router := mux.NewRouter()
	router.HandleFunc("/", func(http.ResponseWriter, *http.Request) {})
	router.Use(cors.Handler)

	request := httptest.NewRequest(http.MethodOptions, "/", nil)
	request.Header.Set("Origin", "http://example.invalid")
	request.Header.Set("Access-Control-Request-Method", "POST")
	request.Header.Set("Access-Control-Request-Headers", "X-API-Key")

	rr := httptest.NewRecorder()
	router.ServeHTTP(rr, request)

	printHeader := func(name string) {
		fmt.Printf("%s: %v\n", name, rr.Header().Values(name))
	}

	fmt.Printf("Status: %d\n", rr.Result().StatusCode)
	printHeader("Access-Control-Allow-Credentials")
	printHeader("Access-Control-Allow-Headers")
	printHeader("Access-Control-Allow-Methods")
	printHeader("Access-Control-Allow-Origin")
	printHeader("Access-Control-Expose-Headers")
	printHeader("Access-Control-Max-Age")
}
Output:

Status: 405
Access-Control-Allow-Credentials: []
Access-Control-Allow-Headers: []
Access-Control-Allow-Methods: []
Access-Control-Allow-Origin: [http://example.invalid]
Access-Control-Expose-Headers: []
Access-Control-Max-Age: []
Example (Methods)
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	// The CORS middleware allows getting the headers for specific requests.
	// This makes it possible to use a different data source for the information.
	// This example shows the trivial case of returning a static value.
	cors := middleware.CORS{
		AllowHeadersFunc: func(*http.Request) []string {
			return []string{"X-API-Key"}
		},
		AllowMethodsFunc: func(*http.Request) []string {
			return []string{http.MethodGet}
		},
		AllowOriginsFunc: func(*http.Request) []string {
			return []string{"http://example.invalid"}
		},
		ExposeHeadersFunc: func(*http.Request) []string {
			return []string{"MY-HEADER"}
		},
	}

	router := mux.NewRouter()
	router.HandleFunc("/", func(http.ResponseWriter, *http.Request) {})
	router.Use(cors.Handler)

	request := httptest.NewRequest(http.MethodGet, "/", nil)
	request.Header.Set("Origin", "http://example.invalid")

	rr := httptest.NewRecorder()
	router.ServeHTTP(rr, request)

	printHeader := func(name string) {
		fmt.Printf("%s: %v\n", name, rr.Header().Values(name))
	}

	fmt.Printf("Status: %d\n", rr.Result().StatusCode)
	printHeader("Access-Control-Allow-Credentials")
	printHeader("Access-Control-Allow-Headers")
	printHeader("Access-Control-Allow-Methods")
	printHeader("Access-Control-Allow-Origin")
	printHeader("Access-Control-Expose-Headers")
	printHeader("Access-Control-Max-Age")
}
Output:

Status: 200
Access-Control-Allow-Credentials: []
Access-Control-Allow-Headers: [X-API-Key]
Access-Control-Allow-Methods: [GET]
Access-Control-Allow-Origin: [http://example.invalid]
Access-Control-Expose-Headers: [MY-HEADER]
Access-Control-Max-Age: []
Example (OriginNotAllowed)
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"time"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	cors := middleware.CORS{
		AllowCredentials: true,
		AllowHeaders:     []string{"X-API-Key"},
		AllowMethods:     []string{http.MethodGet},
		AllowOrigins:     []string{"http://example.invalid"},
		ExposeHeaders:    []string{"MY-HEADER"},
		MaxAge:           90 * time.Second,
	}

	router := mux.NewRouter()
	router.HandleFunc("/", func(http.ResponseWriter, *http.Request) {})
	router.Use(cors.Handler)

	request := httptest.NewRequest(http.MethodOptions, "/", nil)
	request.Header.Set("Origin", "http://wrong.invalid")
	request.Header.Set("Access-Control-Request-Method", http.MethodGet)
	request.Header.Set("Access-Control-Request-Headers", "X-API-Key")

	rr := httptest.NewRecorder()
	router.ServeHTTP(rr, request)

	printHeader := func(name string) {
		fmt.Printf("%s: %v\n", name, rr.Header().Values(name))
	}

	fmt.Printf("Status: %d\n", rr.Result().StatusCode)
	printHeader("Access-Control-Allow-Credentials")
	printHeader("Access-Control-Allow-Headers")
	printHeader("Access-Control-Allow-Methods")
	printHeader("Access-Control-Allow-Origin")
	printHeader("Access-Control-Expose-Headers")
	printHeader("Access-Control-Max-Age")
}
Output:

Status: 403
Access-Control-Allow-Credentials: []
Access-Control-Allow-Headers: []
Access-Control-Allow-Methods: []
Access-Control-Allow-Origin: []
Access-Control-Expose-Headers: []
Access-Control-Max-Age: []
Example (Preflight)
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"time"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	cors := middleware.CORS{
		AllowCredentials: true,
		AllowHeaders:     []string{"X-API-Key"},
		AllowMethods:     []string{http.MethodGet},
		AllowOrigins:     []string{"http://example.invalid"},
		ExposeHeaders:    []string{"MY-HEADER"},
		MaxAge:           90 * time.Second,
	}

	router := mux.NewRouter()
	router.HandleFunc("/", func(http.ResponseWriter, *http.Request) {})
	router.Use(cors.Handler)

	request := httptest.NewRequest(http.MethodOptions, "/", nil)
	request.Header.Set("Origin", "http://example.invalid")
	request.Header.Set("Access-Control-Request-Method", http.MethodGet)
	request.Header.Set("Access-Control-Request-Headers", "X-API-Key")

	rr := httptest.NewRecorder()
	router.ServeHTTP(rr, request)

	printHeader := func(name string) {
		fmt.Printf("%s: %v\n", name, rr.Header().Values(name))
	}

	fmt.Printf("Status: %d\n", rr.Result().StatusCode)
	printHeader("Access-Control-Allow-Credentials")
	printHeader("Access-Control-Allow-Headers")
	printHeader("Access-Control-Allow-Methods")
	printHeader("Access-Control-Allow-Origin")
	printHeader("Access-Control-Expose-Headers")
	printHeader("Access-Control-Max-Age")
}
Output:

Status: 204
Access-Control-Allow-Credentials: [true]
Access-Control-Allow-Headers: [X-API-Key]
Access-Control-Allow-Methods: [GET]
Access-Control-Allow-Origin: [http://example.invalid]
Access-Control-Expose-Headers: []
Access-Control-Max-Age: [90]
Example (Simple)
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"time"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	cors := middleware.CORS{
		AllowCredentials: true,
		AllowHeaders:     []string{"X-API-Key"},
		AllowMethods:     []string{http.MethodGet},
		AllowOrigins:     []string{"http://example.invalid"},
		ExposeHeaders:    []string{"MY-HEADER"},
		MaxAge:           90 * time.Second,
	}

	router := mux.NewRouter()
	router.HandleFunc("/", func(http.ResponseWriter, *http.Request) {})
	router.Use(cors.Handler)

	request := httptest.NewRequest(http.MethodGet, "/", nil)
	request.Header.Set("Origin", "http://example.invalid")

	rr := httptest.NewRecorder()
	router.ServeHTTP(rr, request)

	printHeader := func(name string) {
		fmt.Printf("%s: %v\n", name, rr.Header().Values(name))
	}

	fmt.Printf("Status: %d\n", rr.Result().StatusCode)
	printHeader("Access-Control-Allow-Credentials")
	printHeader("Access-Control-Allow-Headers")
	printHeader("Access-Control-Allow-Methods")
	printHeader("Access-Control-Allow-Origin")
	printHeader("Access-Control-Expose-Headers")
	printHeader("Access-Control-Max-Age")
}
Output:

Status: 200
Access-Control-Allow-Credentials: [true]
Access-Control-Allow-Headers: [X-API-Key]
Access-Control-Allow-Methods: [GET]
Access-Control-Allow-Origin: [http://example.invalid]
Access-Control-Expose-Headers: [MY-HEADER]
Access-Control-Max-Age: [90]

func (*CORS) Handler

func (c *CORS) Handler(h http.Handler) http.Handler

Handler returns a handler that sets and validates the CORS headers on incoming requests. It calls the given handler when the request is valid. Request with method OPTIONS are not passed on unless PassOptions is set to true.

It implements Func.

type Func

type Func func(next http.Handler) http.Handler

Func represents a middleware function. Middleware functions can modify or reject a request before calling the next handler. This can be used to log requests, perform authentication, modify request data, and much more. See http.AllowQuerySemicolons for an example of such a function in net/http.

func MaxBytes

func MaxBytes(n int64) Func

MaxBytes returns a middleware function based on http.MaxBytesHandler.

Example
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	router := mux.NewRouter()
	router.HandleFunc("/", func(_ http.ResponseWriter, r *http.Request) {
		data, _ := io.ReadAll(r.Body)
		defer r.Body.Close()

		fmt.Println(string(data))
	})

	// Limit the body size to 5 bytes.
	router.Use(middleware.MaxBytes(5))

	rr := httptest.NewRecorder()
	router.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/",
		strings.NewReader("Hello World")))

	fmt.Println(rr.Body.String())
}
Output:

Hello

func Timeout

func Timeout(dt time.Duration, msg string) Func

Timeout returns a middleware function using http.TimeoutHandler.

Example
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"time"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	router := mux.NewRouter()
	router.HandleFunc("/", func(_ http.ResponseWriter, r *http.Request) {
		<-r.Context().Done()
	})

	router.Use(middleware.Timeout(time.Millisecond, "timeout!"))

	rr := httptest.NewRecorder()
	router.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/",
		strings.NewReader("Hello World")))

	fmt.Printf("%d %s\n", rr.Code, rr.Body.String())
}
Output:

503 timeout!

type Log

type Log struct {
	// Logger contains the logger used for logging requests.
	// It defaults to `slog.Default`.
	Logger *slog.Logger

	// Level contains the log level on which to log requests.
	// It defaults to log level 'INFO'.
	Level slog.Leveler

	// Message contains the log message accompanying requests.
	// It defaults to 'HTTP Request'.
	Message string

	// Key contains the log key.
	// It defaults to "", which unwraps any group from the value.
	Key string

	// LogValuer contains a function used to format the request.
	// It defaults to LogEntry.LogValue when omitted.
	Value func(entry LogEntry) slog.Value

	// Log the incoming requests instead of finished requests.
	// Note that various fields, like status code are not available at this time.
	LogIncoming bool
}

Log provides a middleware for logging HTTP requests.

Example
package main

import (
	"log/slog"
	"net/http"
	"net/http/httptest"
	"os"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	// Create a logger without timestamps for this example.
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
		ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
			if a.Key == slog.TimeKey && len(groups) == 0 {
				return slog.Attr{}
			}

			return a
		},
	}))

	// Create a log middleware
	logMiddleware := middleware.Log{
		Logger: logger,
		// Custom log value function with only the predictable request fields.
		// See the example on LogEntry.LogValue for the default format.
		Value: func(entry middleware.LogEntry) slog.Value {
			return slog.GroupValue(
				slog.String("method", entry.Request.Method),
				slog.String("host", entry.Request.Host),
				slog.String("path", entry.Request.URL.Path),
				slog.String("protocol", entry.Request.Proto),
				slog.Int("status", entry.Status),
				slog.Int("size_bytes", entry.ResponseSize))
		},
	}

	router := mux.NewRouter()
	router.Use(logMiddleware.Handler)

	router.ServeHTTP(httptest.NewRecorder(),
		httptest.NewRequest(http.MethodGet, "https://example.test/index.html", nil))
}
Output:

level=INFO msg="HTTP Request" method=GET host=example.test path=/index.html protocol=HTTP/1.1 status=404 size_bytes=9
Example (Custom)
package main

import (
	"log/slog"
	"net/http"
	"net/http/httptest"
	"os"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	// Create a logger without timestamps for this example.
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
		ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
			if a.Key == slog.TimeKey && len(groups) == 0 {
				return slog.Attr{}
			}

			return a
		},
	}))

	// Create a log middleware with custom functions.
	logMiddleware := middleware.Log{
		Logger:  logger,
		Level:   slog.LevelWarn,
		Message: "Finished Request",
		Key:     "request",
		// Custom log value function with only the predictable request fields.
		// See the example on LogEntry.LogValue for the default format.
		Value: func(entry middleware.LogEntry) slog.Value {
			return slog.GroupValue(
				slog.String("method", entry.Request.Method),
				slog.String("host", entry.Request.Host),
				slog.String("path", entry.Request.URL.Path),
				slog.String("protocol", entry.Request.Proto),
				slog.Int("status", entry.Status),
				slog.Int("size_bytes", entry.ResponseSize))
		},
	}

	router := mux.NewRouter()
	router.Use(logMiddleware.Handler)

	router.ServeHTTP(httptest.NewRecorder(),
		httptest.NewRequest(http.MethodGet, "https://example.test/index.html", nil))
}
Output:

level=WARN msg="Finished Request" request.method=GET request.host=example.test request.path=/index.html request.protocol=HTTP/1.1 request.status=404 request.size_bytes=9

func (*Log) Handler

func (l *Log) Handler(h http.Handler) http.Handler

Handler returns a handler that logs the request with the parameters set on Log. Any changes in those parameters not be reflected in the log entries.

It implements Func.

type LogEntry

type LogEntry struct {
	// Request contains the incoming request.
	Request *http.Request

	// Status contains the response status.
	// This is not set until the request has finished.
	Status int

	// ResponseSize contains the amount of data written to the response writer (in bytes).
	// This is not set until the request has finished.
	ResponseSize int

	// Duration contains the amount of time taken to handle the request.
	// This is not set until the request has finished.
	Duration time.Duration
}

LogEntry represents an entry logged by the Log middleware.

func (LogEntry) LogValue

func (l LogEntry) LogValue() slog.Value

LogValue returns the log value for the log entry. It implements slog.LogValuer.

Example
package main

import (
	"log/slog"
	"net/http"
	"net/url"
	"os"
	"time"

	"go.hofstra.dev/httputil/middleware"
)

func main() {
	// Create a logger without timestamps for this example.
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
		ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
			if a.Key == slog.TimeKey && len(groups) == 0 {
				return slog.Attr{}
			}

			return a
		},
	}))

	entry := middleware.LogEntry{
		Request: &http.Request{
			RemoteAddr: "[2001:db8:212::17]:8080",
			Method:     http.MethodGet,
			Host:       "example.com",
			Proto:      "HTTP/1.1",
			URL: &url.URL{
				Scheme: "https",
				Host:   "example.com",
				Path:   "/api/v1/logs",
			},
			Header: http.Header{
				"User-Agent": []string{"Go-http-client/1.1"},
			},
		},
		Status:       http.StatusNoContent,
		ResponseSize: 0,
		Duration:     21 * time.Second,
	}

	logger.Info("HTTP Request", "", entry)
}
Output:

level=INFO msg="HTTP Request" remote_addr=[2001:db8:212::17]:8080 status=204 method=GET host=example.com path=/api/v1/logs duration_seconds=21 response_bytes=0 user_agent=Go-http-client/1.1

type SecurityHeaders

type SecurityHeaders struct {
	// Version sets the version of the security headers.
	// Additions in headers or changes in defaults lead to a new version.
	// It defaults to the latest version.
	Version int

	// Disable setting the 'Content-Security-Policy' header.
	DisableContentSecurityPolicy bool

	// Disable setting the 'Cross-Origin-Embedder-Policy' header.
	DisableCrossOriginEmbeddedPolicy bool

	// Disable setting the 'Cross-Origin-Opener-Policy' header.
	DisableCrossOriginOpenerPolicy bool

	// Disable setting the 'Cross-Origin-Resource-Policy' header.
	DisableCrossOriginResourcePolicy bool

	// Disable setting the 'Permissions-Policy' header.
	DisablePermissionsPolicy bool

	// Disable setting the 'Referrer-Policy' header.
	DisableReferrerPolicy bool

	// Disable setting the 'Strict-Transport-Security' header.
	DisableStrictTransportSecurity bool

	// Disable setting the 'X-Content-Type-Options' header.
	DisableContentTypeOptions bool

	// Disable setting the 'X-Frame-Options' header.
	DisableFrameOptions bool
	// contains filtered or unexported fields
}

SecurityHeaders implements a middleware that sets security headers. It sets the following headers by default:

Content-Security-Policy:     	default-src 'self'
Cross-Origin-Embedder-Policy:	require-corp
Cross-Origin-Opener-Policy:		same-origin
Cross-Origin-Resource-Policy:	same-origin
Permissions-Policy:				<disable everything>
Referrer-Policy:        		no-referrer
Strict-Transport-Security:  	max-age=63072000; includeSubdomains; preload
X-Content-Type-Options: 		nosniff
X-Frame-Options:        		DENY

This middleware is versioned with an increase in version when new headers are added. Set the Version field to the desired version to ensure backwards compatibility. Default headers per version are:

  • 1: Content-Security-Policy, Cross-Origin-Embedder-Policy, Cross-Origin-Opener-Policy, Cross-Origin-Resource-Policy, Permissions-Policy, Referrer-Policy, Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options.
Example (Defaults)
package main

import (
	"fmt"
	"maps"
	"net/http"
	"net/http/httptest"
	"slices"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	securityHeaders := middleware.SecurityHeaders{}

	router := mux.NewRouter()
	router.HandleFunc("/", func(http.ResponseWriter, *http.Request) {})
	router.Use(securityHeaders.Handler)

	rr := httptest.NewRecorder()
	router.ServeHTTP(rr, httptest.NewRequest(http.MethodOptions, "/", nil))

	for _, header := range slices.Sorted(maps.Keys(rr.Header())) {
		fmt.Printf("%s: %q\n", header, rr.Header().Get(header))
	}
}
Output:

Content-Security-Policy: "default-src 'self'"
Cross-Origin-Embedder-Policy: "require-corp"
Cross-Origin-Opener-Policy: "same-origin"
Cross-Origin-Resource-Policy: "same-origin"
Permissions-Policy: "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), speaker-selection=(), conversion-measurement=(), focus-without-user-activation=(), hid=(), idle-detection=(), interest-cohort=(), serial=(), sync-script=(), trust-token-redemption=(), unload=(), window-placement=(), vertical-scroll=()"
Referrer-Policy: "no-referrer"
Strict-Transport-Security: "max-age=63072000; includeSubdomains; preload"
X-Content-Type-Options: "nosniff"
X-Frame-Options: "DENY"
Example (DisableAll)
package main

import (
	"fmt"
	"maps"
	"net/http"
	"net/http/httptest"
	"slices"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	securityHeaders := middleware.SecurityHeaders{
		DisableFrameOptions:              true,
		DisableContentTypeOptions:        true,
		DisableReferrerPolicy:            true,
		DisableStrictTransportSecurity:   true,
		DisableContentSecurityPolicy:     true,
		DisableCrossOriginEmbeddedPolicy: true,
		DisableCrossOriginOpenerPolicy:   true,
		DisableCrossOriginResourcePolicy: true,
		DisablePermissionsPolicy:         true,
	}

	router := mux.NewRouter()
	router.HandleFunc("/", func(http.ResponseWriter, *http.Request) {})
	router.Use(securityHeaders.Handler)

	rr := httptest.NewRecorder()
	router.ServeHTTP(rr, httptest.NewRequest(http.MethodOptions, "/", nil))

	for _, header := range slices.Sorted(maps.Keys(rr.Header())) {
		fmt.Printf("%s: %q\n", header, rr.Header().Get(header))
	}
}
Example (V1)
package main

import (
	"fmt"
	"maps"
	"net/http"
	"net/http/httptest"
	"slices"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	securityHeaders := middleware.SecurityHeaders{Version: 1}

	router := mux.NewRouter()
	router.HandleFunc("/", func(http.ResponseWriter, *http.Request) {})
	router.Use(securityHeaders.Handler)

	rr := httptest.NewRecorder()
	router.ServeHTTP(rr, httptest.NewRequest(http.MethodOptions, "/", nil))

	for _, header := range slices.Sorted(maps.Keys(rr.Header())) {
		fmt.Printf("%s: %q\n", header, rr.Header().Get(header))
	}
}
Output:

Content-Security-Policy: "default-src 'self'"
Cross-Origin-Embedder-Policy: "require-corp"
Cross-Origin-Opener-Policy: "same-origin"
Cross-Origin-Resource-Policy: "same-origin"
Permissions-Policy: "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), speaker-selection=(), conversion-measurement=(), focus-without-user-activation=(), hid=(), idle-detection=(), interest-cohort=(), serial=(), sync-script=(), trust-token-redemption=(), unload=(), window-placement=(), vertical-scroll=()"
Referrer-Policy: "no-referrer"
Strict-Transport-Security: "max-age=63072000; includeSubdomains; preload"
X-Content-Type-Options: "nosniff"
X-Frame-Options: "DENY"

func (*SecurityHeaders) Handler

func (s *SecurityHeaders) Handler(next http.Handler) http.Handler

Handler returns a handler that sets the security headers and calls the next handler to handle the request.

It implements Func.

func (*SecurityHeaders) SetContentSecurityPolicy

func (s *SecurityHeaders) SetContentSecurityPolicy(directives ...string)

SetContentSecurityPolicy sets the content security policy. See the MDN documentation for details.

Example
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	securityHeaders := middleware.SecurityHeaders{}
	securityHeaders.SetContentSecurityPolicy(
		"default-src 'self'",
		"img-src 'self' example.com",
	)

	router := mux.NewRouter()
	router.HandleFunc("/", func(http.ResponseWriter, *http.Request) {})
	router.Use(securityHeaders.Handler)

	rr := httptest.NewRecorder()
	router.ServeHTTP(rr, httptest.NewRequest(http.MethodOptions, "/", nil))

	fmt.Println(rr.Header().Get("Content-Security-Policy"))
}
Output:

default-src 'self'; img-src 'self' example.com

func (*SecurityHeaders) SetContentSecurityPolicyReportOnly

func (s *SecurityHeaders) SetContentSecurityPolicyReportOnly(directives ...string)

SetContentSecurityPolicyReportOnly sets the content security policy for reporting only. It must include a 'report-to' directive, which reports to an endpoint set by SecurityHeaders.SetReportingEndpoints. See the MDN documentation for details.

Example
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	securityHeaders := middleware.SecurityHeaders{}
	securityHeaders.SetContentSecurityPolicyReportOnly(
		"default-src 'self'",
		"img-src 'self' example.com",
		"report-to csp-endpoint",
	)

	router := mux.NewRouter()
	router.HandleFunc("/", func(http.ResponseWriter, *http.Request) {})
	router.Use(securityHeaders.Handler)

	rr := httptest.NewRecorder()
	router.ServeHTTP(rr, httptest.NewRequest(http.MethodOptions, "/", nil))

	fmt.Println(rr.Header().Get("Content-Security-Policy-Report-Only"))
}
Output:

default-src 'self'; img-src 'self' example.com; report-to csp-endpoint

func (*SecurityHeaders) SetCrossOriginEmbedderPolicy

func (s *SecurityHeaders) SetCrossOriginEmbedderPolicy(value string)

SetCrossOriginEmbedderPolicy sets the Cross-Origin-Embedder-Policy header. Valid values are 'unsafe-none', 'require-corp' (the default) and 'credentialless'. See the MDN documentation for details.

Example
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	securityHeaders := middleware.SecurityHeaders{}
	securityHeaders.SetCrossOriginEmbedderPolicy("credentialless")

	router := mux.NewRouter()
	router.HandleFunc("/", func(http.ResponseWriter, *http.Request) {})
	router.Use(securityHeaders.Handler)

	rr := httptest.NewRecorder()
	router.ServeHTTP(rr, httptest.NewRequest(http.MethodOptions, "/", nil))

	fmt.Println(rr.Header().Get("Cross-Origin-Embedder-Policy"))
}
Output:

credentialless

func (*SecurityHeaders) SetCrossOriginOpenerPolicy

func (s *SecurityHeaders) SetCrossOriginOpenerPolicy(value string)

SetCrossOriginOpenerPolicy sets the Cross-Origin-Opener-Policy header. Valid values are 'unsafe-none', 'same-origin-allow-popups', 'same-origin' (the default) and 'noopener-allow-popups'. See the MDN documentation for details.

Example
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	securityHeaders := middleware.SecurityHeaders{}
	securityHeaders.SetCrossOriginOpenerPolicy("same-origin-allow-popups")

	router := mux.NewRouter()
	router.HandleFunc("/", func(http.ResponseWriter, *http.Request) {})
	router.Use(securityHeaders.Handler)

	rr := httptest.NewRecorder()
	router.ServeHTTP(rr, httptest.NewRequest(http.MethodOptions, "/", nil))

	fmt.Println(rr.Header().Get("Cross-Origin-Opener-Policy"))
}
Output:

same-origin-allow-popups

func (*SecurityHeaders) SetCrossOriginResourcePolicy

func (s *SecurityHeaders) SetCrossOriginResourcePolicy(value string)

SetCrossOriginResourcePolicy sets the Cross-Origin-Resource-Policy header. Valid values are 'same-site', 'same-origin' (the default) and 'cross-origin'. See the MDN documentation for details.

Example
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	securityHeaders := middleware.SecurityHeaders{}
	securityHeaders.SetCrossOriginResourcePolicy("cross-origin")

	router := mux.NewRouter()
	router.HandleFunc("/", func(http.ResponseWriter, *http.Request) {})
	router.Use(securityHeaders.Handler)

	rr := httptest.NewRecorder()
	router.ServeHTTP(rr, httptest.NewRequest(http.MethodOptions, "/", nil))

	fmt.Println(rr.Header().Get("Cross-Origin-Resource-Policy"))
}
Output:

cross-origin

func (*SecurityHeaders) SetFrameOptionsSameOrigin

func (s *SecurityHeaders) SetFrameOptionsSameOrigin()

SetFrameOptionsSameOrigin sets the referrer policy to 'SAMEORIGIN' instead of the default value of 'DENY'. See the MDN documentation for details.

Example
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	securityHeaders := middleware.SecurityHeaders{}
	securityHeaders.SetFrameOptionsSameOrigin()

	router := mux.NewRouter()
	router.HandleFunc("/", func(http.ResponseWriter, *http.Request) {})
	router.Use(securityHeaders.Handler)

	rr := httptest.NewRecorder()
	router.ServeHTTP(rr, httptest.NewRequest(http.MethodOptions, "/", nil))

	fmt.Println(rr.Header().Get("X-Frame-Options"))
}
Output:

SAMEORIGIN

func (*SecurityHeaders) SetPermissionsPolicy

func (s *SecurityHeaders) SetPermissionsPolicy(reportTo string, value map[string][]string)

SetPermissionsPolicy sets the Permissions-Policy header. The reportTo arguments sets the report-to value for all non-wildcard directives. The value argument is a mapping of the directive (eg "geolocation") to the allow list (eg {"self", "https://example.com"} or {"*"}). Quotes and braces are applied automatically.

See the MDN documentation for details on the header.

Example
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	securityHeaders := middleware.SecurityHeaders{}
	securityHeaders.SetPermissionsPolicy(
		"permissions-endpoint",
		map[string][]string{
			"geolocation":        {"self", "https://example.com"},
			"picture-in-picture": middleware.AllowAll(),
		},
	)

	router := mux.NewRouter()
	router.HandleFunc("/", func(http.ResponseWriter, *http.Request) {})
	router.Use(securityHeaders.Handler)

	rr := httptest.NewRecorder()
	router.ServeHTTP(rr, httptest.NewRequest(http.MethodOptions, "/", nil))

	fmt.Println(rr.Header().Get("Permissions-Policy"))
}
Output:

geolocation=(self "https://example.com");report-to=permissions-endpoint, picture-in-picture=*

func (*SecurityHeaders) SetReferrerPolicy

func (s *SecurityHeaders) SetReferrerPolicy(value string)

SetReferrerPolicy overrides the string used for the referrer policy. See the MDN documentation for details.

Example
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	securityHeaders := middleware.SecurityHeaders{}
	securityHeaders.SetReferrerPolicy("strict-origin-when-cross-origin")

	router := mux.NewRouter()
	router.HandleFunc("/", func(http.ResponseWriter, *http.Request) {})
	router.Use(securityHeaders.Handler)

	rr := httptest.NewRecorder()
	router.ServeHTTP(rr, httptest.NewRequest(http.MethodOptions, "/", nil))

	fmt.Println(rr.Header().Get("Referrer-Policy"))
}
Output:

strict-origin-when-cross-origin

func (*SecurityHeaders) SetReportingEndpoints

func (s *SecurityHeaders) SetReportingEndpoints(endpoints map[string]string)

SetReportingEndpoints sets the Reporting-Endpoints header. The argument is a mapping of the endpoint name and URL.

See the MDN documentation for details on the header.

Example
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	securityHeaders := middleware.SecurityHeaders{}
	securityHeaders.SetReportingEndpoints(map[string]string{
		"csp-endpoint":         "https://example.com/csp-reports",
		"permissions-endpoint": "https://example.com/permissions-policy-reports",
	})

	router := mux.NewRouter()
	router.HandleFunc("/", func(http.ResponseWriter, *http.Request) {})
	router.Use(securityHeaders.Handler)

	rr := httptest.NewRecorder()
	router.ServeHTTP(rr, httptest.NewRequest(http.MethodOptions, "/", nil))

	fmt.Println(rr.Header().Get("Reporting-Endpoints"))
}
Output:

csp-endpoint="https://example.com/csp-reports", permissions-endpoint="https://example.com/permissions-policy-reports"

func (*SecurityHeaders) SetStrictTransportSecurity

func (s *SecurityHeaders) SetStrictTransportSecurity(maxAge time.Duration, includeSubdomains, preload bool)

SetStrictTransportSecurity overrides the string used for the strict transport security. See the MDN documentation for details.

Example
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"time"

	"go.hofstra.dev/httputil/middleware"
	"go.hofstra.dev/httputil/mux"
)

func main() {
	securityHeaders := middleware.SecurityHeaders{}
	securityHeaders.SetStrictTransportSecurity(time.Hour, true, true)

	router := mux.NewRouter()
	router.HandleFunc("/", func(http.ResponseWriter, *http.Request) {})
	router.Use(securityHeaders.Handler)

	rr := httptest.NewRecorder()
	router.ServeHTTP(rr, httptest.NewRequest(http.MethodOptions, "/", nil))

	fmt.Println(rr.Header().Get("Strict-Transport-Security"))
}
Output:

max-age=3600; includeSubdomains; preload

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL