Avatar Ilya Mateyko

go.astrophena.name/base Module

GitHub repository | Commit (26c5f09)

Base Go packages for my projects.

package web

Contents

import "go.astrophena.name/base/web"

Package web provides a collection of functions and types for building web services.

It includes a configurable HTTP server that simplifies common tasks like setting up middleware, serving static files with cache-busting, and graceful shutdown. The package also offers helpers for standardized JSON and HTML responses, including error handling.

Key types and functions

Usage

A simple server can be created and run like this:

mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "Hello, world!")
})

s := &web.Server{
	Mux:  mux,
	Addr: ":8080",
}

if err := s.ListenAndServe(context.Background()); err != nil {
	log.Fatal(err)
}

Index

Examples

Constants

const (
	CSPSelf         = "'self'"
	CSPNone         = "'none'"
	CSPUnsafeInline = "'unsafe-inline'"
	CSPUnsafeEval   = "'unsafe-eval'"
)

CSP source constants.

Variables

var StaticFS = hashfs.NewFS(staticFS)

StaticFS is a hashfs.FS containing the base static resources (like default CSS) served by the Server under the "/static/" path prefix.

If you provide a custom Server.StaticFS, you must use the Server.StaticHashName method to generate correct hashed URLs for all static assets (both embedded and custom).

Functions

func HandleJSON

func HandleJSON[Req, Resp any](logic func(r *http.Request, req Req) (Resp, error)) http.HandlerFunc

HandleJSON provides a wrapper for creating HTTP handlers that work with JSON requests and responses. It simplifies the common pattern of decoding a JSON request, validating it, executing business logic, and encoding a JSON response.

The generic type Req is the expected request body type, and Resp is the success response body type.

The handler performs the following steps:

  1. If the request method is not GET or HEAD, it attempts to decode the request body into a value of type Req. If decoding fails, it sends a 400 Bad Request response.
  2. If the decoded request object implements the Validatable interface, its Validate method is called. If validation fails, a 400 Bad Request response is sent.
  3. The provided logic function is called with the request and the decoded request object.
  4. If the logic function returns an error, RespondJSONError is used to send an appropriate error response. The error can be wrapped with a StatusErr to control the HTTP status code.
  5. If the logic function succeeds, the returned response object of type Resp is sent to the client using RespondJSON with a 200 OK status.

func IsTrustedRequest

func IsTrustedRequest(r *http.Request) bool

IsTrustedRequest reports whether r is a trusted request. A trusted request, when resulting in an error handled by RespondError, will have its underlying error message exposed to the client in the HTML response.

func RespondError

func RespondError(w http.ResponseWriter, r *http.Request, err error)

RespondError writes an error response in HTML format to w and logs the error using logger.Error if error is ErrInternalServerError.

If the error is a StatusErr or wraps it, it extracts the HTTP status code and sets the response status code accordingly. Otherwise, it sets the response status code to http.StatusInternalServerError.

If the request is marked as trusted (see IsTrustedRequest and TrustRequest), the original error message will be included in the HTML response. This is useful for debugging by service administrators.

You can wrap any error with fmt.Errorf to create a StatusErr and set a specific HTTP status code:

// This will set the status code to 404 (Not Found).
web.RespondError(w, r, fmt.Errorf("resource %w", web.ErrNotFound))

func RespondJSON

func RespondJSON(w http.ResponseWriter, response any)

RespondJSON marshals the provided response object as JSON and writes it to the http.ResponseWriter. It sets the Content-Type header to application/json before marshalling. In case of marshalling errors, it writes an internal server error with the error message.

func RespondJSONError

func RespondJSONError(w http.ResponseWriter, r *http.Request, err error)

RespondJSONError writes an error response in JSON format to w and logs the error using logger.Error if error is ErrInternalServerError.

If the error is a StatusErr or wraps it, it extracts the HTTP status code and sets the response status code accordingly. Otherwise, it sets the response status code to http.StatusInternalServerError. The error message is always included in the JSON response.

You can wrap any error with fmt.Errorf to create a StatusErr and set a specific HTTP status code:

// This will set the status code to 404 (Not Found).
web.RespondJSONError(w, r, fmt.Errorf("resource %w", web.ErrNotFound))

func TrustRequest

func TrustRequest(r *http.Request) *http.Request

TrustRequest marks r as a trusted request and returns a new request with the trusted status embedded in its context. This function should typically be used for requests originating from service administrators or other privileged users.

Types

type CSP

type CSP struct {
	DefaultSrc              []string `csp:"default-src"`
	ScriptSrc               []string `csp:"script-src"`
	StyleSrc                []string `csp:"style-src"`
	ImgSrc                  []string `csp:"img-src"`
	ConnectSrc              []string `csp:"connect-src"`
	FontSrc                 []string `csp:"font-src"`
	ObjectSrc               []string `csp:"object-src"`
	MediaSrc                []string `csp:"media-src"`
	FrameSrc                []string `csp:"frame-src"`
	ChildSrc                []string `csp:"child-src"`
	FormAction              []string `csp:"form-action"`
	FrameAncestors          []string `csp:"frame-ancestors"`
	BaseURI                 []string `csp:"base-uri"`
	Sandbox                 []string `csp:"sandbox"`
	PluginTypes             []string `csp:"plugin-types"`
	ReportURI               []string `csp:"report-uri"`
	ReportTo                []string `csp:"report-to"`
	WorkerSrc               []string `csp:"worker-src"`
	ManifestSrc             []string `csp:"manifest-src"`
	PrefetchSrc             []string `csp:"prefetch-src"`
	NavigateTo              []string `csp:"navigate-to"`
	BlockAllMixedContent    bool     `csp:"block-all-mixed-content"`
	UpgradeInsecureRequests bool     `csp:"upgrade-insecure-requests"`
	// contains filtered or unexported fields
}

CSP represents a Content Security Policy. The zero value is an empty policy.

See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy.

func (CSP) Finalize

func (p CSP) Finalize() CSP

Finalize computes and caches the string representation of the policy. CSPs are intended to be immutable; Finalize should be called after a policy is fully constructed.

func (CSP) String

func (p CSP) String() string

String returns the CSP header value.

type CSPMux

type CSPMux struct {
	// contains filtered or unexported fields
}

CSPMux is a multiplexer for Content Security Policies. It matches the URL of each incoming request against a list of registered patterns and returns the policy for the pattern that most closely matches the URL.

func NewCSPMux

func NewCSPMux() *CSPMux

NewCSPMux creates a new CSPMux.

func (*CSPMux) Handle

func (mux *CSPMux) Handle(pattern string, policy CSP)

Handle registers the CSP for the given pattern. If a policy already exists for pattern, Handle panics.

func (*CSPMux) PolicyFor

func (mux *CSPMux) PolicyFor(r *http.Request) (CSP, bool)

PolicyFor returns the CSP for the given request. It finds the best matching pattern and returns its policy. If no pattern matches, it returns a zero CSP and false.

type CheckResponse

type CheckResponse struct {
	Status string `json:"status"`
	OK     bool   `json:"ok"`
}

CheckResponse represents a status of an individual check.

type DebugHandler

type DebugHandler struct {
	// contains filtered or unexported fields
}

DebugHandler is an http.Handler that serves a debugging "homepage", and provides helpers to register more debug endpoints and reports.

The rendered page consists of three sections: header menu, informational key/value pairs and links to other pages.

Callers can add to these sections using the MenuFunc, KV and Link helpers respectively.

Additionally, the Handle method offers a shorthand for correctly registering debug handlers and cross-linking them from /debug/.

Methods of DebugHandler can be safely called by multiple goroutines.

func Debugger

func Debugger(mux *http.ServeMux) *DebugHandler

Debugger returns the DebugHandler registered on mux at /debug/, creating it if necessary.

func (*DebugHandler) Handle

func (d *DebugHandler) Handle(slug, desc string, handler http.Handler)

Handle registers handler at /debug/<slug> and creates a descriptive entry in /debug/ for it.

func (*DebugHandler) HandleFunc

func (d *DebugHandler) HandleFunc(slug, desc string, handler http.HandlerFunc)

HandleFunc is like Handle, but accepts http.HandlerFunc instead of http.Handler.

func (*DebugHandler) KV

func (d *DebugHandler) KV(k string, v any)

KV adds a key/value list item to /debug/.

func (*DebugHandler) KVFunc

func (d *DebugHandler) KVFunc(k string, v func() any)

KVFunc adds a key/value list item to /debug/. v is called on every render of /debug/.

func (d *DebugHandler) Link(url, desc string)

Link adds a URL and description list item to /debug/.

func (*DebugHandler) MenuFunc

func (d *DebugHandler) MenuFunc(f func(*http.Request) []MenuItem)

MenuFunc sets a function that generates custom menu items for /debug/ page header.

func (*DebugHandler) ServeHTTP

func (d *DebugHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements the http.Handler interface.

type HTMLItem

type HTMLItem string

HTMLItem is a MenuItem that can contain arbitrary HTML.

func (HTMLItem) ToHTML

func (hi HTMLItem) ToHTML() template.HTML

type HealthFunc

type HealthFunc func() (status string, ok bool)

HealthFunc is the health check function that reports the state of a particular subsystem.

type HealthHandler

type HealthHandler struct {
	// contains filtered or unexported fields
}

HealthHandler is an HTTP handler that returns information about the health status of the running service.

func Health

func Health(mux *http.ServeMux) *HealthHandler

Health returns the HealthHandler registered on mux at /health, creating it if necessary.

func (*HealthHandler) RegisterFunc

func (h *HealthHandler) RegisterFunc(name string, f HealthFunc)

RegisterFunc registers the health check function by the given name. If the health check function with this name already exists, RegisterFunc panics.

Health check function must be safe for concurrent use.

func (*HealthHandler) ServeHTTP

func (h *HealthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements the http.Handler interface.

type HealthResponse

type HealthResponse struct {
	OK     bool                     `json:"ok"`
	Checks map[string]CheckResponse `json:"checks"`
}

HealthResponse represents a response of the /health endpoint.

type LinkItem

type LinkItem struct {
	Name   string
	Target string
}

LinkItem is a MenuItem that is a link.

func (LinkItem) ToHTML

func (li LinkItem) ToHTML() template.HTML
type MenuItem interface {
	ToHTML() template.HTML
}

MenuItem is a debug page header menu item.

type Middleware

type Middleware func(http.Handler) http.Handler

type Server

type Server struct {
	// Mux is a http.ServeMux to serve.
	Mux *http.ServeMux
	// Debuggable specifies whether to register debug handlers at /debug/.
	Debuggable bool
	// Middleware specifies an optional slice of HTTP middleware that's applied to
	// each request.
	Middleware []Middleware
	// Addr is a network address to listen on (in the form of "host:port").
	Addr string
	// Ready specifies an optional function to be called when the server is ready
	// to serve requests.
	Ready func()
	// StaticFS specifies an optional filesystem containing static assets (like CSS,
	// JS, images) to be served. If provided, it's combined with the embedded
	// StaticFS and served under the "/static/" path prefix.
	// Files in this FS take precedence over the embedded ones if names conflict.
	StaticFS fs.FS
	// CrossOriginProtection configures CSRF protection. Defaults are used if nil.
	CrossOriginProtection *http.CrossOriginProtection
	// CSP is a multiplexer for Content Security Policies.
	// If nil, a default restrictive policy is used.
	CSP *CSPMux
	// contains filtered or unexported fields
}

Server is used to configure the HTTP server started by Server.ListenAndServe.

All fields of Server can't be modified after Server.StaticHashName, Server.ListenAndServe or Server.ServeHTTP is called for a first time.

Example (CustomHandler)

Demonstrates how to use the response helpers within a custom HTTP handler.

package main

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

	"go.astrophena.name/base/web"
)

func main() {
	// A simple struct for our API response.
	type User struct {
		ID   int    `json:"id"`
		Name string `json:"name"`
	}

	users := map[int]User{
		1: {ID: 1, Name: "Alice"},
	}

	// This handler returns a user by ID or an error if the user is not found.
	getUserHandler := func(w http.ResponseWriter, r *http.Request) {
		idStr := r.PathValue("id")
		id, err := strconv.Atoi(idStr)
		if err != nil {
			// Respond with a 400 Bad Request error.
			web.RespondJSONError(w, r, fmt.Errorf("%w: invalid user id", web.ErrBadRequest))
			return
		}

		user, ok := users[id]
		if !ok {
			// Respond with a 404 Not Found error.
			web.RespondJSONError(w, r, fmt.Errorf("%w: user not found", web.ErrNotFound))
			return
		}

		// Respond with the user data as JSON.
		web.RespondJSON(w, user)
	}

	mux := http.NewServeMux()
	mux.HandleFunc("GET /users/{id}", getUserHandler)

	s := &web.Server{Mux: mux}

	// Simulate requests to test the handler.

	// Successful request.
	req1 := httptest.NewRequest(http.MethodGet, "/users/1", nil)
	w1 := httptest.NewRecorder()
	s.ServeHTTP(w1, req1)
	fmt.Println("Successful response:")
	fmt.Println(w1.Body.String())

	// Not found request.
	req2 := httptest.NewRequest(http.MethodGet, "/users/2", nil)
	w2 := httptest.NewRecorder()
	s.ServeHTTP(w2, req2)
	fmt.Println("Not found response:")
	fmt.Println(w2.Body.String())

}

Output:

Successful response:
{
  "id": 1,
  "name": "Alice"
}

Not found response:
{
  "status": "error",
  "error": "not found: user not found"
}
Example (WithDebugAndHealth)

Shows how to set up a server with debugging and health check endpoints.

package main

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

	"go.astrophena.name/base/web"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "Main page")
	})

	s := &web.Server{
		Mux:        mux,
		Debuggable: true, // This enables the /debug/ endpoint.
	}

	// The /health endpoint is enabled by default. We can add a custom check.
	h := web.Health(s.Mux)
	h.RegisterFunc("database", func() (status string, ok bool) {
		// In a real app, you would check the database connection.
		return "connected", true
	})

	// To prevent the example from blocking, we don't actually run ListenAndServe.
	// In a real application, you would call s.ListenAndServe(ctx).

	// Let's test the health endpoint.
	req := httptest.NewRequest(http.MethodGet, "/health", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	fmt.Println(w.Body.String())

}

Output:

{
  "ok": true,
  "checks": {
    "database": {
      "status": "connected",
      "ok": true
    }
  }
}

func (*Server) ListenAndServe

func (s *Server) ListenAndServe(ctx context.Context) error

ListenAndServe starts the HTTP server that can be stopped by canceling ctx.

func (*Server) ServeHTTP

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements the http.Handler interface.

func (*Server) StaticHashName

func (s *Server) StaticHashName(name string) string

StaticHashName returns the cache-busting hashed name for a static file path. If the path exists, its hashed name is returned. Otherwise, the original name is returned.

type StatusErr

type StatusErr int

StatusErr is a sentinel error type used to represent HTTP status code errors.

const (
	// ErrBadRequest represents a bad request error (HTTP 400).
	ErrBadRequest StatusErr = http.StatusBadRequest
	// ErrUnauthorized represents an unauthorized access error (HTTP 401).
	ErrUnauthorized StatusErr = http.StatusUnauthorized
	// ErrForbidden represents a forbidden access error (HTTP 403).
	ErrForbidden StatusErr = http.StatusForbidden
	// ErrNotFound represents a not found error (HTTP 404).
	ErrNotFound StatusErr = http.StatusNotFound
	// ErrMethodNotAllowed represents a method not allowed error (HTTP 405).
	ErrMethodNotAllowed StatusErr = http.StatusMethodNotAllowed
	// ErrInternalServerError represents an internal server error (HTTP 500).
	ErrInternalServerError StatusErr = http.StatusInternalServerError
)

func (StatusErr) Error

func (se StatusErr) Error() string

Error implements the error interface. It returns a lowercase representation of the HTTP status text for the wrapped code.

type Validatable

type Validatable interface {
	Validate() error
}

Validatable is an interface for types that can validate themselves. It is used by HandleJSON to automatically validate request bodies.

Directories

tgauthPackage tgauth provides middleware for handling Telegram authentication.