web API

web

package

API reference for the web package.

F
function

TestRunDoctorDisabled

Parameters

app/web/doctor_disabled_test.go:7-13
func TestRunDoctorDisabled(t *testing.T)

{
	s := New()

	if err := s.runDoctor(); err != nil {
		t.Fatalf("runDoctor() error = %v", err)
	}
}
T
type

methodHandlers

app/web/radix.go:12-12
type methodHandlers map[string]HandlerFunc
S
struct

paramConstraint

app/web/radix.go:14-17
type paramConstraint struct

Fields

Name Type Description
name string
validate func(string) bool
S
struct

radixNode

app/web/radix.go:19-27
type radixNode struct

Fields

Name Type Description
children []*radixNode
segment string
isParam bool
isCatchAll bool
paramName string
constraints []paramConstraint
handlers methodHandlers
S
struct

radixTree

app/web/radix.go:29-32
type radixTree struct

Methods

insert
Method

Parameters

method string
path string
handler HandlerFunc

Returns

error
func (*radixTree) insert(method, path string, handler HandlerFunc) error
{
	t.mu.Lock()
	defer t.mu.Unlock()

	if !validRouteMethod(method) {
		return fmt.Errorf("web: unsupported HTTP method %q", method)
	}
	if handler == nil {
		return fmt.Errorf("web: route handler cannot be nil")
	}
	if err := validateRoutePath(path); err != nil {
		return err
	}
	segments := splitPath(path)

	if len(segments) > 0 && isCatchAll(segments[len(segments)-1]) {
		segments = segments[:len(segments)-1]
		node, err := t.walkOrCreate(t.root, segments)
		if err != nil {
			return err
		}
		paramName := parseCatchAll(path)
		if paramName == "" {
			return fmt.Errorf("web: catch-all parameter name cannot be empty")
		}
		if strings.Contains(paramName, ":") {
			return fmt.Errorf("web: catch-all parameters cannot have constraints")
		}
		for _, child := range node.children {
			if child.isParam {
				return fmt.Errorf("web: catch-all route %s conflicts with a parameter route", path)
			}
			if child.isCatchAll {
				if child.paramName != paramName {
					return fmt.Errorf("web: conflicting catch-all route %s", path)
				}
				if _, exists := child.handlers[method]; exists {
					return fmt.Errorf("web: route %s %s is already registered", method, path)
				}
				child.handlers[method] = handler
				return nil
			}
		}
		catchAllNode := &radixNode{
			isCatchAll: true,
			paramName:  paramName,
			children:   make([]*radixNode, 0),
		}
		node.children = append(node.children, catchAllNode)
		if catchAllNode.handlers == nil {
			catchAllNode.handlers = make(methodHandlers)
		}
		catchAllNode.handlers[method] = handler
		return nil
	}

	node, err := t.walkOrCreate(t.root, segments)
	if err != nil {
		return err
	}
	if node.handlers == nil {
		node.handlers = make(methodHandlers)
	}
	if _, exists := node.handlers[method]; exists {
		return fmt.Errorf("web: route %s %s is already registered", method, path)
	}
	node.handlers[method] = handler
	return nil
}
walkOrCreate
Method

Parameters

node *radixNode
segments []string

Returns

error
func (*radixTree) walkOrCreate(node *radixNode, segments []string) (*radixNode, error)
{
	for _, seg := range segments {
		if isParam(seg) {
			name, constraints, err := parseParamConstraints(seg)
			if err != nil {
				return nil, err
			}
			node, err = findOrCreateParam(node, name, constraints)
			if err != nil {
				return nil, err
			}
		} else {
			if strings.ContainsAny(seg, "{}") {
				return nil, fmt.Errorf("web: malformed route segment %q", seg)
			}
			node = findOrCreateStatic(node, seg)
		}
	}
	return node, nil
}
lookup
Method

Parameters

method string
path string

Returns

map[string]string
func (*radixTree) lookup(method, path string) (HandlerFunc, map[string]string)
{
	t.mu.RLock()
	defer t.mu.RUnlock()

	segments := splitPath(path)
	params := make(map[string]string)
	handler, ok := lookupNode(t.root, method, segments, 0, params)
	if !ok {
		return nil, nil
	}
	return handler, params
}

Fields

Name Type Description
root *radixNode
mu sync.RWMutex
F
function

newRadixTree

Returns

app/web/radix.go:34-38
func newRadixTree() *radixTree

{
	return &radixTree{
		root: &radixNode{children: make([]*radixNode, 0)},
	}
}
F
function

findOrCreateStatic

Parameters

parent
segment
string

Returns

app/web/radix.go:131-144
func findOrCreateStatic(parent *radixNode, segment string) *radixNode

{
	for _, child := range parent.children {
		if !child.isParam && !child.isCatchAll && child.segment == segment {
			return child
		}
	}
	node := &radixNode{
		segment:  segment,
		isParam:  false,
		children: make([]*radixNode, 0),
	}
	parent.children = append(parent.children, node)
	return node
}
F
function

findOrCreateParam

Parameters

parent
paramName
string
constraints

Returns

error
app/web/radix.go:146-169
func findOrCreateParam(parent *radixNode, paramName string, constraints []paramConstraint) (*radixNode, error)

{
	if paramName == "" {
		return nil, fmt.Errorf("web: route parameter name cannot be empty")
	}
	for _, child := range parent.children {
		if child.isCatchAll {
			return nil, fmt.Errorf("web: parameter route %q conflicts with a catch-all route", paramName)
		}
		if child.isParam {
			if child.paramName != paramName || !sameConstraints(child.constraints, constraints) {
				return nil, fmt.Errorf("web: ambiguous parameter route for %q", paramName)
			}
			return child, nil
		}
	}
	node := &radixNode{
		isParam:     true,
		paramName:   paramName,
		constraints: constraints,
		children:    make([]*radixNode, 0),
	}
	parent.children = append(parent.children, node)
	return node, nil
}
F
function

sameConstraints

Parameters

Returns

bool
app/web/radix.go:171-181
func sameConstraints(left, right []paramConstraint) bool

{
	if len(left) != len(right) {
		return false
	}
	for i := range left {
		if left[i].name != right[i].name {
			return false
		}
	}
	return true
}
F
function

lookupNode

Parameters

node
method
string
segments
[]string
index
int
params
map[string]string

Returns

app/web/radix.go:196-251
func lookupNode(node *radixNode, method string, segments []string, index int, params map[string]string) (HandlerFunc, bool)

{
	if index == len(segments) {
		if handler, ok := node.handlers[method]; ok {
			return handler, true
		}
		for _, child := range node.children {
			if !child.isCatchAll {
				continue
			}
			if handler, ok := child.handlers[method]; ok {
				params[child.paramName] = ""
				return handler, true
			}
		}
		return nil, false
	}

	segment := segments[index]
	for _, child := range node.children {
		if child.isParam || child.isCatchAll || child.segment != segment {
			continue
		}
		if handler, ok := lookupNode(child, method, segments, index+1, params); ok {
			return handler, true
		}
	}

	for _, child := range node.children {
		if !child.isParam || !constraintsMatch(child.constraints, segment) {
			continue
		}
		previous, existed := params[child.paramName]
		params[child.paramName] = segment
		if handler, ok := lookupNode(child, method, segments, index+1, params); ok {
			return handler, true
		}
		if existed {
			params[child.paramName] = previous
		} else {
			delete(params, child.paramName)
		}
	}

	for _, child := range node.children {
		if !child.isCatchAll {
			continue
		}
		handler, ok := child.handlers[method]
		if !ok {
			continue
		}
		params[child.paramName] = strings.Join(segments[index:], "/")
		return handler, true
	}
	return nil, false
}
F
function

constraintsMatch

Parameters

constraints
value
string

Returns

bool
app/web/radix.go:253-260
func constraintsMatch(constraints []paramConstraint, value string) bool

{
	for _, constraint := range constraints {
		if !constraint.validate(value) {
			return false
		}
	}
	return true
}
F
function

splitPath

Parameters

path
string

Returns

[]string
app/web/radix.go:262-268
func splitPath(path string) []string

{
	trimmed := strings.Trim(path, "/")
	if trimmed == "" {
		return nil
	}
	return strings.Split(trimmed, "/")
}
F
function

isParam

Parameters

seg
string

Returns

bool
app/web/radix.go:270-272
func isParam(seg string) bool

{
	return len(seg) > 2 && seg[0] == '{' && seg[len(seg)-1] == '}'
}
F
function

isCatchAll

Parameters

seg
string

Returns

bool
app/web/radix.go:274-276
func isCatchAll(seg string) bool

{
	return len(seg) > 3 && seg[0] == '{' && seg[1] == '*' && seg[len(seg)-1] == '}'
}
F
function

parseCatchAll

Parameters

path
string

Returns

string
app/web/radix.go:278-285
func parseCatchAll(path string) string

{
	parts := strings.Split(strings.Trim(path, "/"), "/")
	last := parts[len(parts)-1]
	if isCatchAll(last) {
		return last[2 : len(last)-1]
	}
	return ""
}
F
function

parseParamConstraints

Parameters

seg
string

Returns

string
error
app/web/radix.go:287-331
func parseParamConstraints(seg string) (string, []paramConstraint, error)

{
	inner := seg[1 : len(seg)-1]
	colonIdx := strings.Index(inner, ":")
	if colonIdx < 0 {
		return inner, nil, nil
	}

	name := inner[:colonIdx]
	constraintStr := inner[colonIdx+1:]
	var constraints []paramConstraint

	rawConstraints := strings.Split(constraintStr, ",")
	if strings.HasPrefix(constraintStr, "regex(") {
		rawConstraints = []string{constraintStr}
	}
	for _, c := range rawConstraints {
		switch c {
		case "int":
			constraints = append(constraints, paramConstraint{
				name:     "int",
				validate: isIntConstraint,
			})
		case "alpha":
			constraints = append(constraints, paramConstraint{
				name:     "alpha",
				validate: isAlphaConstraint,
			})
		default:
			if strings.HasPrefix(c, "regex(") && strings.HasSuffix(c, ")") {
				pattern := c[6 : len(c)-1]
				validator, err := isRegexConstraint(pattern)
				if err != nil {
					return "", nil, fmt.Errorf("web: invalid regex constraint %q: %w", pattern, err)
				}
				constraints = append(constraints, paramConstraint{
					name:     c,
					validate: validator,
				})
				continue
			}
			return "", nil, fmt.Errorf("web: unknown route constraint %q", c)
		}
	}
	return name, constraints, nil
}
F
function

isIntConstraint

Parameters

s
string

Returns

bool
app/web/radix.go:333-336
func isIntConstraint(s string) bool

{
	_, err := strconv.Atoi(s)
	return err == nil
}
F
function

isAlphaConstraint

Parameters

s
string

Returns

bool
app/web/radix.go:338-345
func isAlphaConstraint(s string) bool

{
	for _, r := range s {
		if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')) {
			return false
		}
	}
	return true
}
F
function

isRegexConstraint

Parameters

pattern
string

Returns

func(string)
bool
error
app/web/radix.go:347-355
func isRegexConstraint(pattern string) (func(string) bool, error)

{
	re, err := regexp.Compile(pattern)
	if err != nil {
		return nil, err
	}
	return func(s string) bool {
		return re.MatchString(s)
	}, nil
}
F
function

validRouteMethod

Parameters

method
string

Returns

bool
app/web/radix.go:357-366
func validRouteMethod(method string) bool

{
	switch method {
	case http.MethodGet, http.MethodHead, http.MethodPost, http.MethodPut,
		http.MethodPatch, http.MethodDelete, http.MethodConnect,
		http.MethodOptions, http.MethodTrace:
		return true
	default:
		return false
	}
}
F
function

validateRoutePath

Parameters

path
string

Returns

error
app/web/radix.go:368-406
func validateRoutePath(path string) error

{
	if !strings.HasPrefix(path, "/") {
		return fmt.Errorf("web: route path must start with /")
	}
	seen := make(map[string]struct{})
	segments := splitPath(path)
	for index, segment := range segments {
		if !strings.ContainsAny(segment, "{}") {
			continue
		}
		if !isParam(segment) {
			return fmt.Errorf("web: malformed route segment %q", segment)
		}

		name := segment[1 : len(segment)-1]
		if strings.HasPrefix(name, "*") {
			if !isCatchAll(segment) || index != len(segments)-1 {
				return fmt.Errorf("web: catch-all parameter must be last and well formed")
			}
			name = strings.TrimPrefix(name, "*")
			if strings.Contains(name, ":") {
				return fmt.Errorf("web: catch-all parameters cannot have constraints")
			}
		} else if separator := strings.IndexByte(name, ':'); separator >= 0 {
			if _, _, err := parseParamConstraints(segment); err != nil {
				return err
			}
			name = name[:separator]
		}
		if !validRouteParameterName(name) {
			return fmt.Errorf("web: invalid route parameter name %q", name)
		}
		if _, exists := seen[name]; exists {
			return fmt.Errorf("web: duplicate route parameter %q", name)
		}
		seen[name] = struct{}{}
	}
	return nil
}
F
function

validRouteParameterName

Parameters

name
string

Returns

bool
app/web/radix.go:408-429
func validRouteParameterName(name string) bool

{
	if name == "" {
		return false
	}
	for index, character := range name {
		if index == 0 {
			if character != '_' &&
				(character < 'A' || character > 'Z') &&
				(character < 'a' || character > 'z') {
				return false
			}
			continue
		}
		if character != '_' &&
			(character < 'A' || character > 'Z') &&
			(character < 'a' || character > 'z') &&
			(character < '0' || character > '9') {
			return false
		}
	}
	return true
}
F
function

Auth

Auth returns middleware that validates a Bearer token using auth.VerifyToken.
A valid token is attached to the context via auth.Payload.

Parameters

secret
[]byte

Returns

app/web/auth.go:12-38
func Auth(secret []byte) Middleware

{
	if err := auth.ValidateHMACSecret(secret); err != nil {
		panic(err)
	}
	secret = append([]byte(nil), secret...)
	return func(next HandlerFunc) HandlerFunc {
		return func(ctx *Context) error {
			h := ctx.Request.Header.Get("Authorization")
			if h == "" {
				return ctx.JSON(http.StatusUnauthorized, map[string]string{"error": "missing Authorization header"})
			}

			parts := strings.SplitN(h, " ", 2)
			if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
				return ctx.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid Authorization format"})
			}

			payload, err := auth.VerifyToken(parts[1], secret)
			if err != nil {
				return ctx.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid bearer token"})
			}

			ctx.Set("auth.payload", payload)
			return next(ctx)
		}
	}
}
S
struct

PingEndpoint

app/web/handler_test.go:13-16
type PingEndpoint struct

Methods

Handle
Method

Parameters

Returns

any
error
func (*PingEndpoint) Handle(_ context.Context) (any, error)
{
	out := make([]string, e.Times)
	for i := range out {
		out[i] = "pong"
	}
	return pingResponse{Message: out}, nil
}

Fields

Name Type Description
Meta struct{} method:"GET" path:"/ping"
Times int query:"times" default:"1"
S
struct

pingResponse

app/web/handler_test.go:18-20
type pingResponse struct

Fields

Name Type Description
Message []string json:"message"
F
function

TestRegisterHandler_Basic

Parameters

app/web/handler_test.go:30-52
func TestRegisterHandler_Basic(t *testing.T)

{
	s := New()
	b := di.NewBuilder()
	container, _ := b.Build()

	if err := s.RegisterHandler(&PingEndpoint{}, container); err != nil {
		t.Fatalf("RegisterHandler() error = %v", err)
	}

	req := httptest.NewRequest("GET", "/ping", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	if w.Code != 200 {
		t.Errorf("status = %d, want 200, body: %s", w.Code, w.Body.String())
	}

	var resp pingResponse
	json.NewDecoder(w.Body).Decode(&resp)
	if len(resp.Message) != 1 {
		t.Errorf("messages = %d, want 1", len(resp.Message))
	}
}
F
function

TestRegisterHandler_Routes

Parameters

app/web/handler_test.go:54-70
func TestRegisterHandler_Routes(t *testing.T)

{
	s := New()
	b := di.NewBuilder()
	container, _ := b.Build()

	if err := s.RegisterHandler(&PingEndpoint{}, container); err != nil {
		t.Fatalf("RegisterHandler() error = %v", err)
	}

	routes := s.Routes()
	if len(routes) != 1 {
		t.Fatalf("routes = %d, want 1", len(routes))
	}
	if routes[0].Method != "GET" || routes[0].Path != "/ping" {
		t.Errorf("route = %s %s, want GET /ping", routes[0].Method, routes[0].Path)
	}
}
F
function

TestRegisterHandler_WithQuery

Parameters

app/web/handler_test.go:72-94
func TestRegisterHandler_WithQuery(t *testing.T)

{
	s := New()
	b := di.NewBuilder()
	container, _ := b.Build()

	if err := s.RegisterHandler(&PingEndpoint{}, container); err != nil {
		t.Fatalf("RegisterHandler() error = %v", err)
	}

	req := httptest.NewRequest("GET", "/ping?times=3", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	if w.Code != 200 {
		t.Errorf("status = %d, want 200", w.Code)
	}

	var resp pingResponse
	json.NewDecoder(w.Body).Decode(&resp)
	if len(resp.Message) != 3 {
		t.Errorf("messages = %d, want 3", len(resp.Message))
	}
}
F
function

TestRegisterHandler_InvalidQueryReturnsBadRequest

Parameters

app/web/handler_test.go:96-110
func TestRegisterHandler_InvalidQueryReturnsBadRequest(t *testing.T)

{
	s := New()
	container := di.NewBuilder().MustBuild()

	if err := s.RegisterHandler(&PingEndpoint{}, container); err != nil {
		t.Fatalf("RegisterHandler() error = %v", err)
	}

	req := httptest.NewRequest("GET", "/ping?times=invalid", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 400 {
		t.Fatalf("status = %d, want 400", w.Code)
	}
}
S
struct

DiEndpoint

app/web/handler_test.go:112-116
type DiEndpoint struct

Methods

Handle
Method

Parameters

Returns

any
error
func (*DiEndpoint) Handle(_ context.Context) (any, error)
{
	return map[string]string{"greeting": "hello " + e.Name}, nil
}

Fields

Name Type Description
Meta struct{} method:"GET" path:"/greet"
Name string query:"name" default:"world"
Greet string inject:"greet"
F
function

TestRegisterHandler_WithDI

Parameters

app/web/handler_test.go:122-139
func TestRegisterHandler_WithDI(t *testing.T)

{
	s := New()
	b := di.NewBuilder()
	b.Provide("greet", "custom-greeting")
	container, _ := b.Build()

	if err := s.RegisterHandler(&DiEndpoint{}, container); err != nil {
		t.Fatalf("RegisterHandler() error = %v", err)
	}

	req := httptest.NewRequest("GET", "/greet?name=alice", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	if w.Code != 200 {
		t.Errorf("status = %d, want 200", w.Code)
	}
}
S
struct

NoReturnEndpoint

app/web/handler_test.go:141-143
type NoReturnEndpoint struct

Methods

Handle
Method

Parameters

Returns

any
error
func (*NoReturnEndpoint) Handle(_ context.Context) (any, error)
{
	return nil, nil
}

Fields

Name Type Description
Meta struct{} method:"DELETE" path:"/item"
F
function

TestRegisterHandler_NilReturn

Parameters

app/web/handler_test.go:149-165
func TestRegisterHandler_NilReturn(t *testing.T)

{
	s := New()
	b := di.NewBuilder()
	container, _ := b.Build()

	if err := s.RegisterHandler(&NoReturnEndpoint{}, container); err != nil {
		t.Fatalf("RegisterHandler() error = %v", err)
	}

	req := httptest.NewRequest("DELETE", "/item", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	if w.Code != 204 {
		t.Errorf("status = %d, want 204", w.Code)
	}
}
F
function

TestRegisterHandler_RejectsMissingDependency

Parameters

app/web/handler_test.go:167-174
func TestRegisterHandler_RejectsMissingDependency(t *testing.T)

{
	s := New()
	container := di.NewBuilder().MustBuild()

	if err := s.RegisterHandler(&DiEndpoint{}, container); err == nil {
		t.Fatal("RegisterHandler() accepted a missing dependency")
	}
}
F
function

TestRegisterHandler_RejectsDuplicateRoute

Parameters

app/web/handler_test.go:176-186
func TestRegisterHandler_RejectsDuplicateRoute(t *testing.T)

{
	s := New()
	container := di.NewBuilder().MustBuild()

	if err := s.RegisterHandler(&PingEndpoint{}, container); err != nil {
		t.Fatalf("RegisterHandler() error = %v", err)
	}
	if err := s.RegisterHandler(&PingEndpoint{}, container); err == nil {
		t.Fatal("RegisterHandler() accepted a duplicate route")
	}
}
F
function

TestRegisterDefinition

Parameters

app/web/handler_test.go:188-209
func TestRegisterDefinition(t *testing.T)

{
	s := New()
	container := di.NewBuilder().MustBuild()

	err := s.RegisterDefinition(HandlerDefinition{
		Method: "GET",
		Path:   "/static",
		New: func() Handler {
			return &PingEndpoint{}
		},
	}, container)
	if err != nil {
		t.Fatalf("RegisterDefinition() error = %v", err)
	}

	req := httptest.NewRequest("GET", "/static", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 200 {
		t.Fatalf("status = %d, want 200", w.Code)
	}
}
S
struct

BodyEndpoint

app/web/handler_test.go:211-216
type BodyEndpoint struct

Methods

Handle
Method

Parameters

Returns

any
error
func (*BodyEndpoint) Handle(_ context.Context) (any, error)
{
	return e.Body, nil
}

Fields

Name Type Description
Meta struct{} method:"POST" path:"/body"
Body struct { Name string `json:"name"` } body:"json"
F
function

TestRegisterHandler_RejectsUnknownBodyField

Parameters

app/web/handler_test.go:222-236
func TestRegisterHandler_RejectsUnknownBodyField(t *testing.T)

{
	s := New()
	container := di.NewBuilder().MustBuild()
	if err := s.RegisterHandler(&BodyEndpoint{}, container); err != nil {
		t.Fatalf("RegisterHandler() error = %v", err)
	}

	req := httptest.NewRequest("POST", "/body", strings.NewReader(`{"name":"Ada","admin":true}`))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 400 {
		t.Fatalf("status = %d, want 400", w.Code)
	}
}
F
function

TestRegisterHandler_RejectsOversizedBody

Parameters

app/web/handler_test.go:238-252
func TestRegisterHandler_RejectsOversizedBody(t *testing.T)

{
	s := New()
	container := di.NewBuilder().MustBuild()
	if err := s.RegisterHandler(&BodyEndpoint{}, container); err != nil {
		t.Fatalf("RegisterHandler() error = %v", err)
	}

	req := httptest.NewRequest("POST", "/body", strings.NewReader(strings.Repeat("x", maxRequestBodySize+1)))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 413 {
		t.Fatalf("status = %d, want 413", w.Code)
	}
}
F
function

TestRegisterHandler_RejectsWrongBodyContentType

Parameters

app/web/handler_test.go:254-268
func TestRegisterHandler_RejectsWrongBodyContentType(t *testing.T)

{
	s := New()
	container := di.NewBuilder().MustBuild()
	if err := s.RegisterHandler(&BodyEndpoint{}, container); err != nil {
		t.Fatalf("RegisterHandler() error = %v", err)
	}

	req := httptest.NewRequest("POST", "/body", strings.NewReader(`{"name":"Ada"}`))
	req.Header.Set("Content-Type", "text/plain")
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 415 {
		t.Fatalf("status = %d, want 415", w.Code)
	}
}
F
function

TestServer_MapGet

Parameters

app/web/srv_test.go:14-33
func TestServer_MapGet(t *testing.T)

{
	s := New()
	s.MapGet("/hello", func(ctx *Context) error {
		return ctx.JSON(200, map[string]string{"message": "hello"})
	})

	req := httptest.NewRequest("GET", "/hello", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	if w.Code != 200 {
		t.Errorf("status: got %d, want 200", w.Code)
	}

	var resp map[string]string
	json.NewDecoder(w.Body).Decode(&resp)
	if resp["message"] != "hello" {
		t.Errorf("body: got %q, want %q", resp["message"], "hello")
	}
}
F
function

TestServer_Params

Parameters

app/web/srv_test.go:35-50
func TestServer_Params(t *testing.T)

{
	s := New()
	s.MapGet("/users/{id}", func(ctx *Context) error {
		return ctx.JSON(200, map[string]string{"id": ctx.Params["id"]})
	})

	req := httptest.NewRequest("GET", "/users/42", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	var resp map[string]string
	json.NewDecoder(w.Body).Decode(&resp)
	if resp["id"] != "42" {
		t.Errorf("param: got %q, want %q", resp["id"], "42")
	}
}
F
function

TestServer_NotFound

Parameters

app/web/srv_test.go:52-61
func TestServer_NotFound(t *testing.T)

{
	s := New()
	req := httptest.NewRequest("GET", "/missing", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	if w.Code != 404 {
		t.Errorf("status: got %d, want 404", w.Code)
	}
}
F
function

TestServer_Middleware

Parameters

app/web/srv_test.go:63-82
func TestServer_Middleware(t *testing.T)

{
	s := New()
	s.Use(RequestID())

	s.MapGet("/test", func(ctx *Context) error {
		id, _ := ctx.Get("request_id")
		return ctx.JSON(200, map[string]any{"request_id": id})
	})

	req := httptest.NewRequest("GET", "/test", nil)
	req.Header.Set("X-Request-ID", "test-123")
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	var resp map[string]any
	json.NewDecoder(w.Body).Decode(&resp)
	if resp["request_id"] != "test-123" {
		t.Errorf("middleware: got %v, want test-123", resp["request_id"])
	}
}
F
function

TestServer_RecoveryDoesNotExposePanic

Parameters

app/web/srv_test.go:84-101
func TestServer_RecoveryDoesNotExposePanic(t *testing.T)

{
	s := New()
	s.Use(Recovery())
	s.MapGet("/panic", func(ctx *Context) error {
		panic("private failure detail")
	})

	req := httptest.NewRequest("GET", "/panic", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 500 {
		t.Fatalf("status = %d, want 500", w.Code)
	}
	if strings.Contains(w.Body.String(), "private failure detail") ||
		strings.Contains(w.Body.String(), "goroutine") {
		t.Fatalf("response exposes panic details: %s", w.Body.String())
	}
}
F
function

TestServer_DoesNotExposeInternalError

Parameters

app/web/srv_test.go:103-118
func TestServer_DoesNotExposeInternalError(t *testing.T)

{
	s := New()
	s.MapGet("/failure", func(ctx *Context) error {
		return errors.New("database connection private detail")
	})

	req := httptest.NewRequest("GET", "/failure", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 500 {
		t.Fatalf("status = %d, want 500", w.Code)
	}
	if strings.Contains(w.Body.String(), "database") {
		t.Fatalf("response exposes internal error: %s", w.Body.String())
	}
}
F
function

TestServer_ExposesExplicitHTTPError

Parameters

app/web/srv_test.go:120-135
func TestServer_ExposesExplicitHTTPError(t *testing.T)

{
	s := New()
	s.MapGet("/teapot", func(ctx *Context) error {
		return Error(418, "short and stout")
	})

	req := httptest.NewRequest("GET", "/teapot", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 418 {
		t.Fatalf("status = %d, want 418", w.Code)
	}
	if !strings.Contains(w.Body.String(), "short and stout") {
		t.Fatalf("response lost explicit error: %s", w.Body.String())
	}
}
F
function

TestServer_Group

Parameters

app/web/srv_test.go:137-151
func TestServer_Group(t *testing.T)

{
	s := New()
	api := s.Group("/api")
	api.MapGet("/users", func(ctx *Context) error {
		return ctx.JSON(200, map[string]string{"path": "/api/users"})
	})

	req := httptest.NewRequest("GET", "/api/users", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	if w.Code != 200 {
		t.Errorf("group route: got %d, want 200", w.Code)
	}
}
F
function

TestServer_Routes

Parameters

app/web/srv_test.go:153-174
func TestServer_Routes(t *testing.T)

{
	s := New()
	s.MapGet("/hello", func(ctx *Context) error {
		ctx.String(200, "ok")
		return nil
	})
	s.MapPost("/data", func(ctx *Context) error {
		ctx.String(201, "ok")
		return nil
	})

	routes := s.Routes()
	if len(routes) != 2 {
		t.Fatalf("routes = %d, want 2", len(routes))
	}
	if routes[0].Method != "GET" || routes[0].Path != "/hello" {
		t.Errorf("route[0] = %s %s, want GET /hello", routes[0].Method, routes[0].Path)
	}
	if routes[1].Method != "POST" || routes[1].Path != "/data" {
		t.Errorf("route[1] = %s %s, want POST /data", routes[1].Method, routes[1].Path)
	}
}
F
function

TestServer_HTTPTimeoutDefaults

Parameters

app/web/srv_test.go:176-187
func TestServer_HTTPTimeoutDefaults(t *testing.T)

{
	server := New().httpServer(":8080")
	if server.ReadHeaderTimeout <= 0 ||
		server.ReadTimeout <= 0 ||
		server.WriteTimeout <= 0 ||
		server.IdleTimeout <= 0 {
		t.Fatalf("HTTP timeouts are not configured: %#v", server)
	}
	if server.MaxHeaderBytes != maxRequestBodySize {
		t.Fatalf("MaxHeaderBytes = %d, want %d", server.MaxHeaderBytes, maxRequestBodySize)
	}
}
F
function

TestServer_CORS

Parameters

app/web/srv_test.go:189-207
func TestServer_CORS(t *testing.T)

{
	s := New()
	s.Use(CORS("*"))
	s.MapGet("/test", func(ctx *Context) error {
		ctx.String(200, "ok")
		return nil
	})

	req := httptest.NewRequest("OPTIONS", "/test", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	if w.Code != 204 {
		t.Errorf("CORS preflight: got %d, want 204", w.Code)
	}
	if w.Header().Get("Access-Control-Allow-Origin") != "*" {
		t.Errorf("CORS header: got %q", w.Header().Get("Access-Control-Allow-Origin"))
	}
}
F
function

TestServer_CORSSelectsMatchingOrigin

Parameters

app/web/srv_test.go:209-229
func TestServer_CORSSelectsMatchingOrigin(t *testing.T)

{
	server := New()
	server.Use(CORS("https://one.example", "https://two.example"))
	if err := server.MapGet("/cors", func(ctx *Context) error {
		ctx.String(http.StatusOK, "ok")
		return nil
	}); err != nil {
		t.Fatal(err)
	}

	request := httptest.NewRequest(http.MethodGet, "/cors", nil)
	request.Header.Set("Origin", "https://two.example")
	response := httptest.NewRecorder()
	server.ServeHTTP(response, request)
	if got := response.Header().Get("Access-Control-Allow-Origin"); got != "https://two.example" {
		t.Fatalf("Access-Control-Allow-Origin = %q", got)
	}
	if !strings.Contains(response.Header().Get("Vary"), "Origin") {
		t.Fatalf("Vary = %q, want Origin", response.Header().Get("Vary"))
	}
}
F
function

TestServer_CompressWritesGzip

Parameters

app/web/srv_test.go:231-257
func TestServer_CompressWritesGzip(t *testing.T)

{
	s := New()
	s.Use(Compress())
	s.MapGet("/compressed", func(ctx *Context) error {
		return ctx.JSON(200, map[string]string{"message": "hello"})
	})

	req := httptest.NewRequest("GET", "/compressed", nil)
	req.Header.Set("Accept-Encoding", "gzip")
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Header().Get("Content-Encoding") != "gzip" {
		t.Fatalf("Content-Encoding = %q", w.Header().Get("Content-Encoding"))
	}
	reader, err := gzip.NewReader(w.Body)
	if err != nil {
		t.Fatalf("gzip.NewReader() error = %v", err)
	}
	defer reader.Close()
	var body map[string]string
	if err := json.NewDecoder(reader).Decode(&body); err != nil {
		t.Fatalf("Decode() error = %v", err)
	}
	if body["message"] != "hello" {
		t.Fatalf("message = %q", body["message"])
	}
}
F
function

TestServer_CompressRestoresWriterAfterPanic

Parameters

app/web/srv_test.go:259-279
func TestServer_CompressRestoresWriterAfterPanic(t *testing.T)

{
	s := New()
	s.Use(Recovery())
	s.Use(Compress())
	if err := s.MapGet("/panic", func(*Context) error {
		panic("private failure")
	}); err != nil {
		t.Fatal(err)
	}

	req := httptest.NewRequest("GET", "/panic", nil)
	req.Header.Set("Accept-Encoding", "gzip")
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != http.StatusInternalServerError {
		t.Fatalf("status = %d, want %d", w.Code, http.StatusInternalServerError)
	}
	if strings.Contains(w.Body.String(), "private failure") {
		t.Fatalf("response exposes panic details: %s", w.Body.String())
	}
}
F
function

TestServer_CompressRequiresGzipEncodingToken

Parameters

app/web/srv_test.go:281-299
func TestServer_CompressRequiresGzipEncodingToken(t *testing.T)

{
	s := New()
	s.Use(Compress())
	if err := s.MapGet("/plain", func(ctx *Context) error {
		return ctx.JSON(http.StatusOK, map[string]string{"message": "hello"})
	}); err != nil {
		t.Fatal(err)
	}

	for _, encoding := range []string{"xgzip", "gzip;q=0"} {
		req := httptest.NewRequest("GET", "/plain", nil)
		req.Header.Set("Accept-Encoding", encoding)
		w := httptest.NewRecorder()
		s.ServeHTTP(w, req)
		if got := w.Header().Get("Content-Encoding"); got != "" {
			t.Fatalf("Content-Encoding for %q = %q, want empty", encoding, got)
		}
	}
}
F
function

TestServer_CompressHandlesJSONEncodingError

Parameters

app/web/srv_test.go:301-320
func TestServer_CompressHandlesJSONEncodingError(t *testing.T)

{
	s := New()
	s.Use(Compress())
	if err := s.MapGet("/invalid", func(ctx *Context) error {
		return ctx.JSON(http.StatusOK, make(chan int))
	}); err != nil {
		t.Fatal(err)
	}

	req := httptest.NewRequest("GET", "/invalid", nil)
	req.Header.Set("Accept-Encoding", "gzip")
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != http.StatusInternalServerError {
		t.Fatalf("status = %d, want %d", w.Code, http.StatusInternalServerError)
	}
	if got := w.Header().Get("Content-Encoding"); got != "" {
		t.Fatalf("Content-Encoding = %q, want empty on encoding error", got)
	}
}
F
function

TestServer_Bind

Parameters

app/web/srv_test.go:322-340
func TestServer_Bind(t *testing.T)

{
	s := New()
	s.MapPost("/data", func(ctx *Context) error {
		var body map[string]string
		if err := ctx.Bind(&body); err != nil {
			return err
		}
		return ctx.JSON(200, body)
	})

	req := httptest.NewRequest("POST", "/data", strings.NewReader(`{"key":"value"}`))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	if w.Code != 200 {
		t.Errorf("bind: got %d, want 200", w.Code)
	}
}
F
function

TestServer_BindRejectsUnknownJSONField

Parameters

app/web/srv_test.go:342-358
func TestServer_BindRejectsUnknownJSONField(t *testing.T)

{
	s := New()
	s.MapPost("/data", func(ctx *Context) error {
		var body struct {
			Name string `json:"name"`
		}
		return ctx.Bind(&body)
	})

	req := httptest.NewRequest("POST", "/data", strings.NewReader(`{"name":"Ada","admin":true}`))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 400 {
		t.Fatalf("status = %d, want 400", w.Code)
	}
}
F
function

TestServer_BindRejectsOversizedBody

Parameters

app/web/srv_test.go:360-375
func TestServer_BindRejectsOversizedBody(t *testing.T)

{
	s := New()
	s.MapPost("/data", func(ctx *Context) error {
		var body map[string]string
		return ctx.Bind(&body)
	})

	body := `{"key":"` + strings.Repeat("x", maxRequestBodySize) + `"}`
	req := httptest.NewRequest("POST", "/data", strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 413 {
		t.Fatalf("status = %d, want 413", w.Code)
	}
}
F
function

TestServer_BindRejectsUnsupportedMediaType

Parameters

app/web/srv_test.go:377-391
func TestServer_BindRejectsUnsupportedMediaType(t *testing.T)

{
	s := New()
	s.MapPost("/data", func(ctx *Context) error {
		var body map[string]string
		return ctx.Bind(&body)
	})

	req := httptest.NewRequest("POST", "/data", strings.NewReader("name=Ada"))
	req.Header.Set("Content-Type", "text/plain")
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 415 {
		t.Fatalf("status = %d, want 415", w.Code)
	}
}
F
function

TestServer_FormBindUsesExplicitFields

Parameters

app/web/srv_test.go:393-420
func TestServer_FormBindUsesExplicitFields(t *testing.T)

{
	s := New()
	s.MapPost("/form", func(ctx *Context) error {
		var body struct {
			Name    string `form:"name"`
			IsAdmin bool   `json:"-"`
		}
		if err := ctx.Bind(&body); err != nil {
			return err
		}
		return ctx.JSON(200, body)
	})

	req := httptest.NewRequest(
		"POST",
		"/form",
		strings.NewReader("name=Ada&IsAdmin=true"),
	)
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 200 {
		t.Fatalf("status = %d, want 200", w.Code)
	}
	if strings.Contains(w.Body.String(), "true") {
		t.Fatalf("form bound an unlisted field: %s", w.Body.String())
	}
}
F
function

TestHealthEndpoint

Parameters

app/web/srv_test.go:422-435
func TestHealthEndpoint(t *testing.T)

{
	s := New()
	s.MapGet("/health", HealthEndpoint(func(ctx context.Context) error {
		return nil
	}))

	req := httptest.NewRequest("GET", "/health", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	if w.Code != 200 {
		t.Errorf("health: got %d, want 200", w.Code)
	}
}
F
function

TestServer_IntConstraint

Parameters

app/web/srv_test.go:437-456
func TestServer_IntConstraint(t *testing.T)

{
	s := New()
	s.MapGet("/users/{id:int}", func(ctx *Context) error {
		return ctx.JSON(200, map[string]string{"id": ctx.Params["id"]})
	})

	req := httptest.NewRequest("GET", "/users/42", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 200 {
		t.Errorf("int constraint valid: got %d, want 200", w.Code)
	}

	req = httptest.NewRequest("GET", "/users/abc", nil)
	w = httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 404 {
		t.Errorf("int constraint invalid: got %d, want 404", w.Code)
	}
}
F
function

TestServer_AlphaConstraint

Parameters

app/web/srv_test.go:458-477
func TestServer_AlphaConstraint(t *testing.T)

{
	s := New()
	s.MapGet("/items/{slug:alpha}", func(ctx *Context) error {
		return ctx.JSON(200, map[string]string{"slug": ctx.Params["slug"]})
	})

	req := httptest.NewRequest("GET", "/items/hello", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 200 {
		t.Errorf("alpha constraint valid: got %d, want 200", w.Code)
	}

	req = httptest.NewRequest("GET", "/items/hello123", nil)
	w = httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 404 {
		t.Errorf("alpha constraint invalid: got %d, want 404", w.Code)
	}
}
F
function

TestServer_CatchAll

Parameters

app/web/srv_test.go:479-522
func TestServer_CatchAll(t *testing.T)

{
	s := New()
	if err := s.MapGet("/static/{*filepath}", func(ctx *Context) error {
		return ctx.JSON(200, map[string]string{"path": ctx.Params["filepath"]})
	}); err != nil {
		t.Fatal(err)
	}
	if err := s.MapPost("/static/{*filepath}", func(ctx *Context) error {
		ctx.Response.WriteHeader(http.StatusCreated)
		return nil
	}); err != nil {
		t.Fatalf("MapPost() for existing catch-all: %v", err)
	}

	req := httptest.NewRequest("GET", "/static/css/main.css", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 200 {
		t.Fatalf("catch-all: got %d, want 200", w.Code)
	}
	var resp map[string]string
	json.NewDecoder(w.Body).Decode(&resp)
	if resp["path"] != "css/main.css" {
		t.Errorf("catch-all path: got %q, want %q", resp["path"], "css/main.css")
	}

	req = httptest.NewRequest("GET", "/static/a/b/c/d", nil)
	w = httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 200 {
		t.Fatalf("deep catch-all: got %d, want 200", w.Code)
	}
	json.NewDecoder(w.Body).Decode(&resp)
	if resp["path"] != "a/b/c/d" {
		t.Errorf("deep catch-all: got %q, want %q", resp["path"], "a/b/c/d")
	}

	req = httptest.NewRequest("POST", "/static/css/main.css", nil)
	w = httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != http.StatusCreated {
		t.Errorf("POST catch-all: got %d, want %d", w.Code, http.StatusCreated)
	}
}
F
function

TestServer_RouteLookupBacktracksFromStaticBranch

Parameters

app/web/srv_test.go:524-547
func TestServer_RouteLookupBacktracksFromStaticBranch(t *testing.T)

{
	server := New()
	if err := server.MapGet("/users/new/details", func(ctx *Context) error {
		ctx.String(http.StatusOK, "static")
		return nil
	}); err != nil {
		t.Fatal(err)
	}
	if err := server.MapGet("/users/{id}/settings", func(ctx *Context) error {
		ctx.String(http.StatusOK, ctx.Params["id"])
		return nil
	}); err != nil {
		t.Fatal(err)
	}

	response := httptest.NewRecorder()
	server.ServeHTTP(
		response,
		httptest.NewRequest(http.MethodGet, "/users/new/settings", nil),
	)
	if response.Code != http.StatusOK || response.Body.String() != "new" {
		t.Fatalf("response = (%d, %q), want (200, new)", response.Code, response.Body.String())
	}
}
F
function

TestServer_RouteLookupBacktracksAcrossMethods

Parameters

app/web/srv_test.go:549-572
func TestServer_RouteLookupBacktracksAcrossMethods(t *testing.T)

{
	server := New()
	if err := server.MapGet("/items/static", func(ctx *Context) error {
		ctx.String(http.StatusOK, "static")
		return nil
	}); err != nil {
		t.Fatal(err)
	}
	if err := server.MapPost("/items/{id}", func(ctx *Context) error {
		ctx.String(http.StatusCreated, ctx.Params["id"])
		return nil
	}); err != nil {
		t.Fatal(err)
	}

	response := httptest.NewRecorder()
	server.ServeHTTP(
		response,
		httptest.NewRequest(http.MethodPost, "/items/static", nil),
	)
	if response.Code != http.StatusCreated || response.Body.String() != "static" {
		t.Fatalf("response = (%d, %q), want (201, static)", response.Code, response.Body.String())
	}
}
F
function

TestServer_RejectsMalformedCatchAllRoutes

Parameters

app/web/srv_test.go:574-590
func TestServer_RejectsMalformedCatchAllRoutes(t *testing.T)

{
	for _, path := range []string{
		"/static/{*}",
		"/static/{*path}/suffix",
		"/static/{*path:int}",
		"/static/{{id}}",
		"/static/{id}/{id}",
		"/static/{bad-name}",
	} {
		t.Run(path, func(t *testing.T) {
			server := New()
			if err := server.MapGet(path, func(*Context) error { return nil }); err == nil {
				t.Fatalf("MapGet(%q) accepted a malformed catch-all", path)
			}
		})
	}
}
F
function

TestServer_RejectsParameterCatchAllConflicts

Parameters

app/web/srv_test.go:592-612
func TestServer_RejectsParameterCatchAllConflicts(t *testing.T)

{
	handler := func(*Context) error { return nil }
	for _, register := range []func(*Server) error{
		func(server *Server) error {
			if err := server.MapGet("/items/{id}", handler); err != nil {
				return err
			}
			return server.MapPost("/items/{*rest}", handler)
		},
		func(server *Server) error {
			if err := server.MapGet("/items/{*rest}", handler); err != nil {
				return err
			}
			return server.MapPost("/items/{id}", handler)
		},
	} {
		if err := register(New()); err == nil {
			t.Fatal("router accepted conflicting parameter and catch-all routes")
		}
	}
}
F
function

TestServer_MethodMultiplex

Parameters

app/web/srv_test.go:614-653
func TestServer_MethodMultiplex(t *testing.T)

{
	s := New()
	s.MapGet("/items/{id}", func(ctx *Context) error {
		return ctx.JSON(200, map[string]string{"method": "GET"})
	})
	s.MapPost("/items/{id}", func(ctx *Context) error {
		return ctx.JSON(201, map[string]string{"method": "POST"})
	})
	s.MapDelete("/items/{id}", func(ctx *Context) error {
		return ctx.JSON(200, map[string]string{"method": "DELETE"})
	})

	req := httptest.NewRequest("GET", "/items/1", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 200 {
		t.Errorf("GET: got %d, want 200", w.Code)
	}

	req = httptest.NewRequest("POST", "/items/1", nil)
	w = httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 201 {
		t.Errorf("POST: got %d, want 201", w.Code)
	}

	req = httptest.NewRequest("DELETE", "/items/1", nil)
	w = httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 200 {
		t.Errorf("DELETE: got %d, want 200", w.Code)
	}

	req = httptest.NewRequest("PUT", "/items/1", nil)
	w = httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 404 {
		t.Errorf("PUT not registered: got %d, want 404", w.Code)
	}
}
F
function

TestServer_RegexConstraint

Parameters

app/web/srv_test.go:655-674
func TestServer_RegexConstraint(t *testing.T)

{
	s := New()
	s.MapGet("/files/{name:regex(^[a-z]+\\.txt$)}", func(ctx *Context) error {
		return ctx.JSON(200, map[string]string{"name": ctx.Params["name"]})
	})

	req := httptest.NewRequest("GET", "/files/readme.txt", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 200 {
		t.Errorf("regex valid: got %d, want 200, body: %s", w.Code, w.Body.String())
	}

	req = httptest.NewRequest("GET", "/files/README.TXT", nil)
	w = httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 404 {
		t.Errorf("regex invalid: got %d, want 404", w.Code)
	}
}
F
function

TestServer_RejectsUnknownConstraint

Parameters

app/web/srv_test.go:676-683
func TestServer_RejectsUnknownConstraint(t *testing.T)

{
	s := New()
	if err := s.MapGet("/items/{id:innt}", func(ctx *Context) error {
		return nil
	}); err == nil {
		t.Fatal("MapGet() accepted an unknown constraint")
	}
}
F
function

TestServer_RejectsInvalidRegexConstraint

Parameters

app/web/srv_test.go:685-692
func TestServer_RejectsInvalidRegexConstraint(t *testing.T)

{
	s := New()
	if err := s.MapGet("/items/{id:regex([)}", func(ctx *Context) error {
		return nil
	}); err == nil {
		t.Fatal("MapGet() accepted an invalid regex constraint")
	}
}
F
function

TestServer_RejectsAmbiguousParameterRoute

Parameters

app/web/srv_test.go:694-706
func TestServer_RejectsAmbiguousParameterRoute(t *testing.T)

{
	s := New()
	if err := s.MapGet("/items/{id}", func(ctx *Context) error {
		return nil
	}); err != nil {
		t.Fatalf("MapGet() error = %v", err)
	}
	if err := s.MapGet("/items/{name}", func(ctx *Context) error {
		return nil
	}); err == nil {
		t.Fatal("MapGet() accepted an ambiguous parameter route")
	}
}
F
function

TestRateLimitRejectsInvalidConfiguration

Parameters

app/web/ratelimit_test.go:8-15
func TestRateLimitRejectsInvalidConfiguration(t *testing.T)

{
	if _, err := RateLimit(0, 1); err == nil {
		t.Fatal("RateLimit() accepted a zero rate")
	}
	if _, err := RateLimit(1, 0); err == nil {
		t.Fatal("RateLimit() accepted a zero burst")
	}
}
F
function

TestRateLimitSeparatesClientsWithoutBlocking

Parameters

app/web/ratelimit_test.go:17-51
func TestRateLimitSeparatesClientsWithoutBlocking(t *testing.T)

{
	middleware, err := RateLimit(1, 1)
	if err != nil {
		t.Fatalf("RateLimit() error = %v", err)
	}
	server := New()
	server.Use(middleware)
	server.MapGet("/", func(ctx *Context) error {
		return ctx.JSON(200, map[string]bool{"ok": true})
	})

	request := httptest.NewRequest("GET", "/", nil)
	request.RemoteAddr = "192.0.2.1:1000"
	response := httptest.NewRecorder()
	server.ServeHTTP(response, request)
	if response.Code != 200 {
		t.Fatalf("first client status = %d", response.Code)
	}

	request = httptest.NewRequest("GET", "/", nil)
	request.RemoteAddr = "192.0.2.1:1001"
	response = httptest.NewRecorder()
	server.ServeHTTP(response, request)
	if response.Code != 429 {
		t.Fatalf("limited client status = %d, want 429", response.Code)
	}

	request = httptest.NewRequest("GET", "/", nil)
	request.RemoteAddr = "192.0.2.2:1000"
	response = httptest.NewRecorder()
	server.ServeHTTP(response, request)
	if response.Code != 200 {
		t.Fatalf("second client status = %d, want 200", response.Code)
	}
}
S
struct

Context

Context holds the request state for a single HTTP request.

app/web/srv.go:34-40
type Context struct

Methods

Set
Method

Set stores a key-value pair in the request context.

Parameters

key string
val any
func (*Context) Set(key string, val any)
{
	if c.values == nil {
		c.values = make(map[string]any)
	}
	c.values[key] = val
}
Get
Method

Get retrieves a value from the request context by key.

Parameters

key string

Returns

any
bool
func (*Context) Get(key string) (any, bool)
{
	v, ok := c.values[key]
	return v, ok
}
JSON
Method

JSON writes a JSON response with the given status code.

Parameters

code int
v any

Returns

error
func (*Context) JSON(code int, v any) error
{
	data, err := json.Marshal(v)
	if err != nil {
		return err
	}
	data = append(data, '\n')
	c.Response.Header().Set("Content-Type", "application/json; charset=utf-8")
	c.Response.WriteHeader(code)
	_, err = c.Response.Write(data)
	return err
}
String
Method

String writes a plain text response with the given status code.

Parameters

code int
s string

Returns

error
func (*Context) String(code int, s string) error
{
	c.Response.Header().Set("Content-Type", "text/plain; charset=utf-8")
	c.Response.WriteHeader(code)
	_, err := c.Response.Write([]byte(s))
	return err
}
Bind
Method

Bind decodes the request body into v based on the Content-Type header.

Parameters

v any

Returns

error
func (*Context) Bind(v any) error
{
	c.Request.Body = http.MaxBytesReader(c.Response, c.Request.Body, maxRequestBodySize)
	ct := c.Request.Header.Get("Content-Type")
	contentType, _, err := mime.ParseMediaType(ct)
	if err != nil {
		return Error(http.StatusUnsupportedMediaType, fmt.Sprintf("web: unsupported content type: %s", ct))
	}
	if isJSONContentType(ct) {
		decoder := json.NewDecoder(c.Request.Body)
		decoder.DisallowUnknownFields()
		if err := decoder.Decode(v); err != nil {
			return requestBodyError(err)
		}
		var trailing any
		if err := decoder.Decode(&trailing); err != io.EOF {
			if err == nil {
				return Error(http.StatusBadRequest, "web: request body contains multiple JSON values")
			}
			return requestBodyError(err)
		}
		return nil
	}
	if contentType == "application/x-www-form-urlencoded" {
		if err := c.Request.ParseForm(); err != nil {
			return requestBodyError(err)
		}
		if err := bindForm(c.Request.Form, v); err != nil {
			return Error(http.StatusBadRequest, err.Error())
		}
		return nil
	}
	return Error(http.StatusUnsupportedMediaType, fmt.Sprintf("web: unsupported content type: %s", ct))
}
Validate
Method

Validate runs validation rules on the target object and returns the collected validation errors. Returns nil if there are no errors.

Parameters

target any
func (*Context) Validate(target any) validation.Errors
{
	return validation.New().Validate(target)
}

BindAndValidate decodes the request body into target and then runs validation. Returns a validation.Errors slice if decoding or validation fails.

Parameters

target any
func (*Context) BindAndValidate(target any) validation.Errors
{
	if err := c.Bind(target); err != nil {
		return validation.Errors{{Field: "", Message: err.Error()}}
	}
	return c.Validate(target)
}

Fields

Name Type Description
Request *http.Request
Response http.ResponseWriter
Params map[string]string
Ctx context.Context
values map[string]any
F
function

bindForm

Parameters

form
map[string][]string
v
any

Returns

error
app/web/srv.go:112-134
func bindForm(form map[string][]string, v any) error

{
	val := reflect.ValueOf(v)
	if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Struct {
		return fmt.Errorf("web: form binding requires pointer to struct")
	}
	elem := val.Elem()
	for i := 0; i < elem.NumField(); i++ {
		field := elem.Field(i)
		if !field.CanSet() {
			continue
		}
		fieldName, ok := elem.Type().Field(i).Tag.Lookup("form")
		if !ok || fieldName == "" || fieldName == "-" {
			continue
		}
		if vals, ok := form[fieldName]; ok && len(vals) > 0 {
			if err := setFieldValue(field, vals[0]); err != nil {
				return fmt.Errorf("web: form field %s: %w", fieldName, err)
			}
		}
	}
	return nil
}
F
function

setFieldValue

Parameters

value
string

Returns

error
app/web/srv.go:136-162
func setFieldValue(field reflect.Value, value string) error

{
	switch field.Kind() {
	case reflect.String:
		field.SetString(value)
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		n, err := strconv.ParseInt(value, 10, 64)
		if err != nil {
			return err
		}
		field.SetInt(n)
	case reflect.Float32, reflect.Float64:
		n, err := strconv.ParseFloat(value, 64)
		if err != nil {
			return err
		}
		field.SetFloat(n)
	case reflect.Bool:
		b, err := strconv.ParseBool(value)
		if err != nil {
			return err
		}
		field.SetBool(b)
	default:
		return fmt.Errorf("unsupported type %s", field.Kind())
	}
	return nil
}
F
function

requestBodyError

Parameters

err
error

Returns

error
app/web/srv.go:164-170
func requestBodyError(err error) error

{
	var tooLarge *http.MaxBytesError
	if errors.As(err, &tooLarge) {
		return Error(http.StatusRequestEntityTooLarge, "web: request body exceeds 1 MiB")
	}
	return Error(http.StatusBadRequest, fmt.Sprintf("web: invalid request body: %v", err))
}
T
type

HandlerFunc

HandlerFunc is the handler signature for web routes.

app/web/srv.go:173-173
type HandlerFunc func(*Context) error
T
type

Middleware

Middleware wraps a HandlerFunc, returning a new HandlerFunc.

app/web/srv.go:176-176
type Middleware func(HandlerFunc) HandlerFunc
S
struct

route

app/web/srv.go:178-182
type route struct

Fields

Name Type Description
method string
path string
handler HandlerFunc
S
struct

RouteInfo

RouteInfo describes a registered route.

app/web/srv.go:185-188
type RouteInfo struct

Fields

Name Type Description
Method string
Path string
S
struct

RouteDefinition

RouteDefinition describes a raw HTTP route for batch registration.

app/web/srv.go:191-196
type RouteDefinition struct

Fields

Name Type Description
Method string
Path string
Handler HandlerFunc
Middleware []Middleware
S
struct

Server

Server is a minimal API HTTP server with routing and middleware support.

app/web/srv.go:199-207
type Server struct

Methods

runDoctor
Method

Returns

error
func (*Server) runDoctor() error
{
	return nil
}
runDoctor
Method

Returns

error
func (*Server) runDoctor() error
{
	routes := s.Routes()
	doctorRoutes := make([]doctor.Route, 0, len(routes))
	for _, route := range routes {
		doctorRoutes = append(doctorRoutes, doctor.Route{
			Method: route.Method,
			Path:   route.Path,
		})
	}
	return doctor.Run(doctor.Source{Routes: doctorRoutes})
}
Group
Method

Group creates a route group with a common prefix and optional middleware.

Parameters

prefix string
mw ...Middleware

Returns

func (*Server) Group(prefix string, mw ...Middleware) *group
{
	g := &group{prefix: prefix, middleware: mw, server: s}
	s.mu.Lock()
	s.groups = append(s.groups, g)
	s.mu.Unlock()
	return g
}
Use
Method

Use registers global middleware on the server.

Parameters

func (*Server) Use(mw Middleware)
{
	s.mu.Lock()
	s.middleware = append(s.middleware, mw)
	s.mu.Unlock()
}
MapGet
Method

MapGet registers a GET route at the given path.

Parameters

path string
handler HandlerFunc
mw ...Middleware

Returns

error
func (*Server) MapGet(path string, handler HandlerFunc, mw ...Middleware) error
{
	return s.addRoute("GET", path, handler, mw...)
}
MapPost
Method

MapPost registers a POST route at the given path.

Parameters

path string
handler HandlerFunc
mw ...Middleware

Returns

error
func (*Server) MapPost(path string, handler HandlerFunc, mw ...Middleware) error
{
	return s.addRoute("POST", path, handler, mw...)
}
MapPut
Method

MapPut registers a PUT route at the given path.

Parameters

path string
handler HandlerFunc
mw ...Middleware

Returns

error
func (*Server) MapPut(path string, handler HandlerFunc, mw ...Middleware) error
{
	return s.addRoute("PUT", path, handler, mw...)
}
MapDelete
Method

MapDelete registers a DELETE route at the given path.

Parameters

path string
handler HandlerFunc
mw ...Middleware

Returns

error
func (*Server) MapDelete(path string, handler HandlerFunc, mw ...Middleware) error
{
	return s.addRoute("DELETE", path, handler, mw...)
}
addRoute
Method

Parameters

method string
path string
handler HandlerFunc
mw ...Middleware

Returns

error
func (*Server) addRoute(method, path string, handler HandlerFunc, mw ...Middleware) error
{
	return s.RegisterRoutes(RouteDefinition{
		Method:     method,
		Path:       path,
		Handler:    handler,
		Middleware: mw,
	})
}

RegisterRoutes validates and registers raw routes as one operation.

Parameters

definitions ...RouteDefinition

Returns

error
func (*Server) RegisterRoutes(definitions ...RouteDefinition) error
{
	s.mu.Lock()
	defer s.mu.Unlock()

	prepared := make([]route, 0, len(definitions))
	check := newRadixTree()
	for _, existing := range s.routes {
		if err := check.insert(existing.method, existing.path, existing.handler); err != nil {
			return err
		}
	}
	for _, definition := range definitions {
		chained := chainMiddleware(definition.Handler, definition.Middleware...)
		if err := check.insert(definition.Method, definition.Path, chained); err != nil {
			return err
		}
		prepared = append(prepared, route{
			method:  definition.Method,
			path:    definition.Path,
			handler: chained,
		})
	}
	for _, item := range prepared {
		if err := s.tree.insert(item.method, item.path, item.handler); err != nil {
			return err
		}
		s.routes = append(s.routes, item)
	}
	return nil
}
Routes
Method

Routes returns the registered route metadata.

Returns

func (*Server) Routes() []RouteInfo
{
	s.mu.RLock()
	defer s.mu.RUnlock()

	routes := make([]RouteInfo, 0, len(s.routes))
	for _, route := range s.routes {
		routes = append(routes, RouteInfo{Method: route.method, Path: route.path})
	}
	return routes
}
ServeHTTP
Method

ServeHTTP implements http.Handler, dispatching requests through global middleware and routes.

func (*Server) ServeHTTP(w http.ResponseWriter, r *http.Request)
{
	s.mu.RLock()
	globalMW := make([]Middleware, len(s.middleware))
	copy(globalMW, s.middleware)
	s.mu.RUnlock()

	ctx := &Context{Request: r, Response: w, Ctx: r.Context()}
	ctx.values = make(map[string]any)

	handler := func(c *Context) error {
		return s.serveRoutesWithContext(c)
	}

	wrapped := chainMiddleware(handler, globalMW...)
	if err := wrapped(ctx); err != nil {
		ctx.Response.Header().Set("Content-Type", "application/json; charset=utf-8")
		ctx.Response.WriteHeader(statusFromError(err))
		json.NewEncoder(ctx.Response).Encode(map[string]string{"error": publicErrorMessage(err)})
	}
}

Parameters

ctx *Context

Returns

error
func (*Server) serveRoutesWithContext(ctx *Context) error
{
	s.mu.RLock()
	handler, params := s.tree.lookup(ctx.Request.Method, ctx.Request.URL.Path)
	s.mu.RUnlock()
	if handler == nil {
		return Error(http.StatusNotFound, "not found")
	}

	ctx.Params = params
	return handler(ctx)
}

ListenAndServe starts the server on the given address.

Parameters

addr string

Returns

error
func (*Server) ListenAndServe(addr string) error
{
	if err := s.runDoctor(); err != nil {
		return err
	}
	return s.serve(addr, "", "")
}

ListenAndServeTLS starts the server with TLS on the given address.

Parameters

addr string
certFile string
keyFile string

Returns

error
func (*Server) ListenAndServeTLS(addr, certFile, keyFile string) error
{
	if err := s.runDoctor(); err != nil {
		return err
	}

	return s.serve(addr, certFile, keyFile)
}
serve
Method

Parameters

addr string
certFile string
keyFile string

Returns

error
func (*Server) serve(addr, certFile, keyFile string) error
{
	server, err := s.prepareHTTPServer(addr)
	if err != nil {
		return err
	}
	listener, err := net.Listen("tcp", addr)
	if err != nil {
		s.mu.Lock()
		if s.server == server {
			s.server = nil
		}
		s.mu.Unlock()
		return err
	}
	s.mu.Lock()
	close(s.started)
	s.mu.Unlock()
	if certFile != "" {
		return server.ServeTLS(listener, certFile, keyFile)
	}
	return server.Serve(listener)
}

Parameters

addr string

Returns

error
func (*Server) prepareHTTPServer(addr string) (*http.Server, error)
{
	server := s.httpServer(addr)
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.server != nil {
		return nil, fmt.Errorf("web: server already started")
	}
	s.server = server
	if s.started == nil {
		s.started = make(chan struct{})
	}
	return server, nil
}
Started
Method

Started reports when the underlying HTTP server has been prepared.

Returns

<-chan struct{}
func (*Server) Started() <-chan struct{}
{
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.started == nil {
		s.started = make(chan struct{})
		if s.server != nil {
			close(s.started)
		}
	}
	return s.started
}
httpServer
Method

Parameters

addr string

Returns

func (*Server) httpServer(addr string) *http.Server
{
	return &http.Server{
		Addr:              addr,
		Handler:           s,
		ReadHeaderTimeout: defaultReadHeaderTimeout,
		ReadTimeout:       defaultReadTimeout,
		WriteTimeout:      defaultWriteTimeout,
		IdleTimeout:       defaultIdleTimeout,
		MaxHeaderBytes:    maxRequestBodySize,
	}
}
Shutdown
Method

Shutdown gracefully shuts down the server.

Parameters

Returns

error
func (*Server) Shutdown(ctx context.Context) error
{
	s.mu.RLock()
	server := s.server
	s.mu.RUnlock()
	if server == nil {
		return fmt.Errorf("web: server not started")
	}
	return server.Shutdown(ctx)
}

RegisterHandler registers a struct that implements Handler as an endpoint. The struct must have method and path tags: type MyEndpoint struct { Pattern `method:"GET" path:"/api/v1/ping"` Times int `query:"times" default:"1"` } The container is used for dependency injection and bind for field population.

Parameters

prototype Handler
container *di.Container

Returns

error
func (*Server) RegisterHandler(prototype Handler, container *di.Container) error
{
	definition, err := DefinitionFromHandler(prototype)
	if err != nil {
		return err
	}
	return s.RegisterDefinition(definition, container)
}

RegisterDefinition registers a statically described handler.

Parameters

Returns

error
func (*Server) RegisterDefinition(def HandlerDefinition, container *di.Container) error
{
	return s.RegisterDefinitions(container, def)
}

ValidateDefinitions checks static handlers without changing the server.

Parameters

container *di.Container
defs ...HandlerDefinition

Returns

error
func (*Server) ValidateDefinitions(container *di.Container, defs ...HandlerDefinition) error
{
	prepared, err := s.prepareDefinitions(container, defs)
	if err != nil {
		return err
	}
	s.mu.RLock()
	defer s.mu.RUnlock()
	return validatePreparedDefinitions(s.routes, prepared)
}

RegisterDefinitions validates and registers static handlers as one operation.

Parameters

container *di.Container
defs ...HandlerDefinition

Returns

error
func (*Server) RegisterDefinitions(container *di.Container, defs ...HandlerDefinition) error
{
	prepared, err := s.prepareDefinitions(container, defs)
	if err != nil {
		return err
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	if err := validatePreparedDefinitions(s.routes, prepared); err != nil {
		return err
	}
	for _, item := range prepared {
		if err := s.tree.insert(item.meta.method, item.meta.path, item.handler); err != nil {
			return err
		}
		s.routes = append(s.routes, route{
			method:  item.meta.method,
			path:    item.meta.path,
			handler: item.handler,
		})
	}
	return nil
}

Parameters

container *di.Container

Returns

func (*Server) prepareDefinitions(container *di.Container, defs []HandlerDefinition) ([]preparedDefinition, error)
{
	prepared := make([]preparedDefinition, 0, len(defs))
	for _, def := range defs {
		item, err := s.prepareDefinition(container, def)
		if err != nil {
			return nil, err
		}
		prepared = append(prepared, item)
	}
	return prepared, nil
}

Parameters

Returns

func (*Server) prepareDefinition(container *di.Container, def HandlerDefinition) (preparedDefinition, error)
{
	if def.Method == "" || def.Path == "" {
		return preparedDefinition{}, fmt.Errorf("web: handler definition requires method and path")
	}
	if def.New == nil {
		return preparedDefinition{}, fmt.Errorf("web: handler definition requires a constructor")
	}
	prototype := def.New()
	if prototype == nil {
		return preparedDefinition{}, fmt.Errorf("web: handler constructor returned nil")
	}
	if container != nil {
		if err := container.ValidateTarget(prototype); err != nil {
			return preparedDefinition{}, fmt.Errorf("web: invalid handler %T: %w", prototype, err)
		}
	}

	meta := &handlerMeta{
		new:       def.New,
		name:      fmt.Sprintf("%T", prototype),
		method:    def.Method,
		path:      def.Path,
		container: container,
		jsonBody:  hasJSONBody(prototype),
	}

	handler := s.buildHandler(meta)
	return preparedDefinition{meta: meta, handler: handler}, nil
}
buildHandler
Method

Parameters

meta *handlerMeta

Returns

func (*Server) buildHandler(meta *handlerMeta) HandlerFunc
{
	return func(ctx *Context) error {
		handler := meta.new()

		if meta.container != nil {
			if err := meta.container.Inject(handler); err != nil {
				return fmt.Errorf("web: inject %s: %w", meta.name, err)
			}
		}

		b := bind.New()
		if len(ctx.Params) > 0 {
			b.FromPath(func(key string) string {
				return ctx.Params[key]
			})
		}
		b.FromQuery(ctx.Request)
		b.FromHeader(ctx.Request)
		if err := b.Bind(handler); err != nil {
			return Error(http.StatusBadRequest, fmt.Sprintf("web: invalid request input: %v", err))
		}

		if meta.jsonBody && ctx.Request.Body != nil && ctx.Request.ContentLength != 0 {
			if !isJSONContentType(ctx.Request.Header.Get("Content-Type")) {
				return Error(http.StatusUnsupportedMediaType, "web: JSON body requires application/json content type")
			}
			body, err := io.ReadAll(io.LimitReader(ctx.Request.Body, maxRequestBodySize+1))
			if err != nil {
				return fmt.Errorf("web: read request body: %w", err)
			}
			if len(body) > maxRequestBodySize {
				return Error(http.StatusRequestEntityTooLarge, "web: request body exceeds 1 MiB")
			}
			if len(body) > 0 {
				if err := bind.New().BindJSON(handler, body); err != nil {
					return Error(http.StatusBadRequest, fmt.Sprintf("web: invalid request body: %v", err))
				}
			}
		}

		result, err := handler.Handle(ctx.Request.Context())
		if err != nil {
			return err
		}
		if result == nil {
			ctx.Response.WriteHeader(http.StatusNoContent)
			return nil
		}
		return ctx.JSON(http.StatusOK, result)
	}
}

Fields

Name Type Description
middleware []Middleware
routes []route
groups []*group
mu sync.RWMutex
server *http.Server
tree *radixTree
started chan struct{}
T
type

Option

Option configures a Server.

app/web/srv.go:210-210
type Option options.Option[Server]
F
function

New

New creates a new Server with optional configuration.

Parameters

opts
...Option

Returns

app/web/srv.go:213-222
func New(opts ...Option) *Server

{
	s := &Server{
		tree:    newRadixTree(),
		started: make(chan struct{}),
	}
	for _, opt := range opts {
		opt(s)
	}
	return s
}
S
struct

group

app/web/srv.go:224-228
type group struct

Methods

MapGet
Method

MapGet registers a GET route in the group.

Parameters

path string
handler HandlerFunc

Returns

error
func (*group) MapGet(path string, handler HandlerFunc) error
{
	return g.server.MapGet(g.prefix+path, handler, g.middleware...)
}
MapPost
Method

MapPost registers a POST route in the group.

Parameters

path string
handler HandlerFunc

Returns

error
func (*group) MapPost(path string, handler HandlerFunc) error
{
	return g.server.MapPost(g.prefix+path, handler, g.middleware...)
}
MapPut
Method

MapPut registers a PUT route in the group.

Parameters

path string
handler HandlerFunc

Returns

error
func (*group) MapPut(path string, handler HandlerFunc) error
{
	return g.server.MapPut(g.prefix+path, handler, g.middleware...)
}
MapDelete
Method

MapDelete registers a DELETE route in the group.

Parameters

path string
handler HandlerFunc

Returns

error
func (*group) MapDelete(path string, handler HandlerFunc) error
{
	return g.server.MapDelete(g.prefix+path, handler, g.middleware...)
}

Fields

Name Type Description
prefix string
middleware []Middleware
server *Server
F
function

chainMiddleware

Parameters

mw
...Middleware

Returns

app/web/srv.go:339-344
func chainMiddleware(h HandlerFunc, mw ...Middleware) HandlerFunc

{
	for i := len(mw) - 1; i >= 0; i-- {
		h = mw[i](h)
	}
	return h
}
F
function

matchRoute

Parameters

pattern
string
path
string

Returns

map[string]string
bool
app/web/srv.go:380-398
func matchRoute(pattern, path string) (map[string]string, bool)

{
	patternParts := strings.Split(strings.Trim(pattern, "/"), "/")
	pathParts := strings.Split(strings.Trim(path, "/"), "/")

	if len(patternParts) != len(pathParts) {
		return nil, false
	}

	params := make(map[string]string)
	for i := 0; i < len(patternParts); i++ {
		if strings.HasPrefix(patternParts[i], "{") && strings.HasSuffix(patternParts[i], "}") {
			paramName := patternParts[i][1 : len(patternParts[i])-1]
			params[paramName] = pathParts[i]
		} else if patternParts[i] != pathParts[i] {
			return nil, false
		}
	}
	return params, true
}
F
function

JSON

JSON writes a JSON response using the context.

Parameters

ctx
code
int
v
any

Returns

error
app/web/srv.go:491-493
func JSON(ctx *Context, code int, v any) error

{
	return ctx.JSON(code, v)
}
F
function

Error

Error creates an HTTP error with a status code and message.

Parameters

code
int
msg
string

Returns

error
app/web/srv.go:496-498
func Error(code int, msg string) error

{
	return &httpError{Code: code, Message: msg}
}
S
struct

httpError

app/web/srv.go:500-503
type httpError struct

Methods

Error
Method

Returns

string
func (*httpError) Error() string
{
	return e.Message
}
HTTPCode
Method

HTTPCode returns the HTTP status code from the error.

Returns

int
func (*httpError) HTTPCode() int
{
	return e.Code
}

Fields

Name Type Description
Code int
Message string
S
struct

panicError

app/web/srv.go:514-517
type panicError struct

Methods

Error
Method

Returns

string
func (*panicError) Error() string
{
	return "internal server error"
}
HTTPCode
Method

Returns

int
func (*panicError) HTTPCode() int
{
	return http.StatusInternalServerError
}
Unwrap
Method

Returns

error
func (*panicError) Unwrap() error
{
	if err, ok := e.recovered.(error); ok {
		return err
	}
	return nil
}
Stack
Method

Returns

string
func (*panicError) Stack() string
{
	return e.stack
}
Recovered
Method

Returns

any
func (*panicError) Recovered() any
{
	return e.recovered
}

Fields

Name Type Description
recovered any
stack string
F
function

Recovery

Recovery returns middleware that catches panics and converts them to errors with stack traces.

Returns

app/web/srv.go:543-557
func Recovery() Middleware

{
	return func(next HandlerFunc) HandlerFunc {
		return func(ctx *Context) (err error) {
			defer func() {
				if r := recover(); r != nil {
					err = &panicError{
						recovered: r,
						stack:     string(debug.Stack()),
					}
				}
			}()
			return next(ctx)
		}
	}
}
F
function

Logger

Logger returns middleware that logs request method, path, duration, and status.

Returns

app/web/srv.go:560-569
func Logger() Middleware

{
	return func(next HandlerFunc) HandlerFunc {
		return func(ctx *Context) error {
			start := time.Now()
			err := next(ctx)
			fmt.Printf("%s %q %v %d\n", ctx.Request.Method, ctx.Request.URL.Path, time.Since(start), statusFromError(err))
			return err
		}
	}
}
F
function

CORS

CORS returns middleware that sets CORS headers. Preflight OPTIONS requests get 204.

Parameters

allowOrigins
...string

Returns

app/web/srv.go:572-599
func CORS(allowOrigins ...string) Middleware

{
	allowAny := len(allowOrigins) == 0
	allowed := make(map[string]struct{}, len(allowOrigins))
	for _, origin := range allowOrigins {
		if origin == "*" {
			allowAny = true
		}
		allowed[origin] = struct{}{}
	}
	return func(next HandlerFunc) HandlerFunc {
		return func(ctx *Context) error {
			origin := ctx.Request.Header.Get("Origin")
			if allowAny {
				ctx.Response.Header().Set("Access-Control-Allow-Origin", "*")
			} else if _, ok := allowed[origin]; ok && origin != "" {
				ctx.Response.Header().Set("Access-Control-Allow-Origin", origin)
				ctx.Response.Header().Add("Vary", "Origin")
			}
			ctx.Response.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
			ctx.Response.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
			if ctx.Request.Method == "OPTIONS" {
				ctx.Response.WriteHeader(http.StatusNoContent)
				return nil
			}
			return next(ctx)
		}
	}
}
F
function

RequestID

RequestID returns middleware that sets and propagates X-Request-ID.

Returns

app/web/srv.go:602-614
func RequestID() Middleware

{
	return func(next HandlerFunc) HandlerFunc {
		return func(ctx *Context) error {
			id := ctx.Request.Header.Get("X-Request-ID")
			if id == "" {
				id = fmt.Sprintf("%d", time.Now().UnixNano())
			}
			ctx.Set("request_id", id)
			ctx.Response.Header().Set("X-Request-ID", id)
			return next(ctx)
		}
	}
}
F
function

Compress

Compress returns middleware that writes gzip responses when requested.

Returns

app/web/srv.go:617-643
func Compress() Middleware

{
	return func(next HandlerFunc) HandlerFunc {
		return func(ctx *Context) error {
			if !acceptsGzip(ctx.Request.Header.Get("Accept-Encoding")) {
				return next(ctx)
			}

			original := ctx.Response
			writer := &gzipResponseWriter{
				ResponseWriter: original,
				writer:         gzip.NewWriter(original),
			}
			ctx.Response = writer
			defer func() {
				ctx.Response = original
			}()

			err := next(ctx)
			if writer.compressed {
				if closeErr := writer.writer.Close(); err == nil {
					err = closeErr
				}
			}
			return err
		}
	}
}
F
function

acceptsGzip

Parameters

value
string

Returns

bool
app/web/srv.go:645-666
func acceptsGzip(value string) bool

{
	for _, item := range strings.Split(value, ",") {
		parts := strings.Split(item, ";")
		if !strings.EqualFold(strings.TrimSpace(parts[0]), "gzip") {
			continue
		}
		quality := 1.0
		for _, parameter := range parts[1:] {
			name, raw, ok := strings.Cut(strings.TrimSpace(parameter), "=")
			if !ok || !strings.EqualFold(name, "q") {
				continue
			}
			parsed, err := strconv.ParseFloat(raw, 64)
			if err != nil {
				return false
			}
			quality = parsed
		}
		return quality > 0
	}
	return false
}
S
struct

gzipResponseWriter

app/web/srv.go:668-673
type gzipResponseWriter struct

Methods

WriteHeader
Method

Parameters

status int
func (*gzipResponseWriter) WriteHeader(status int)
{
	if status < http.StatusOK || status == http.StatusNoContent || status == http.StatusNotModified {
		w.passthrough = true
		w.ResponseWriter.WriteHeader(status)
		return
	}
	w.prepare()
	w.ResponseWriter.WriteHeader(status)
}
Write
Method

Parameters

content []byte

Returns

int
error
func (*gzipResponseWriter) Write(content []byte) (int, error)
{
	if w.passthrough {
		return w.ResponseWriter.Write(content)
	}
	w.prepare()
	return w.writer.Write(content)
}
prepare
Method
func (*gzipResponseWriter) prepare()
{
	if w.compressed {
		return
	}
	w.Header().Set("Content-Encoding", "gzip")
	w.Header().Del("Content-Length")
	w.Header().Add("Vary", "Accept-Encoding")
	w.compressed = true
}

Fields

Name Type Description
writer *gzip.Writer
compressed bool
passthrough bool
F
function

HealthEndpoint

HealthEndpoint returns a handler that reports health status based on the checker function.

Parameters

checker
func(ctx context.Context) error

Returns

app/web/srv.go:704-711
func HealthEndpoint(checker func(ctx context.Context) error) HandlerFunc

{
	return func(ctx *Context) error {
		if err := checker(ctx.Ctx); err != nil {
			return ctx.JSON(http.StatusServiceUnavailable, map[string]string{"status": "unhealthy"})
		}
		return ctx.JSON(http.StatusOK, map[string]string{"status": "healthy"})
	}
}
F
function

publicErrorMessage

Parameters

err
error

Returns

string
app/web/srv.go:713-726
func publicErrorMessage(err error) string

{
	var recovered *panicError
	if errors.As(err, &recovered) {
		return "internal server error"
	}
	var explicit *httpError
	if errors.As(err, &explicit) {
		return explicit.Message
	}
	if statusFromError(err) >= http.StatusInternalServerError {
		return "internal server error"
	}
	return err.Error()
}
F
function

statusFromError

Parameters

err
error

Returns

int
app/web/srv.go:728-743
func statusFromError(err error) int

{
	if err == nil {
		return http.StatusOK
	}
	for {
		if he, ok := err.(interface{ HTTPCode() int }); ok {
			return he.HTTPCode()
		}
		if e := errors.Unwrap(err); e != nil {
			err = e
			continue
		}
		break
	}
	return http.StatusInternalServerError
}
F
function

TestAuth_MissingHeader

Parameters

app/web/auth_test.go:13-27
func TestAuth_MissingHeader(t *testing.T)

{
	s := New()
	s.Use(Auth(authTestSecret))
	s.MapGet("/api", func(ctx *Context) error {
		return ctx.JSON(200, map[string]string{"ok": "yes"})
	})

	req := httptest.NewRequest("GET", "/api", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	if w.Code != http.StatusUnauthorized {
		t.Errorf("status: got %d, want 401", w.Code)
	}
}
F
function

TestAuth_InvalidToken

Parameters

app/web/auth_test.go:29-44
func TestAuth_InvalidToken(t *testing.T)

{
	s := New()
	s.Use(Auth(authTestSecret))
	s.MapGet("/api", func(ctx *Context) error {
		return ctx.JSON(200, map[string]string{"ok": "yes"})
	})

	req := httptest.NewRequest("GET", "/api", nil)
	req.Header.Set("Authorization", "Bearer badtoken")
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	if w.Code != http.StatusUnauthorized {
		t.Errorf("status: got %d, want 401", w.Code)
	}
}
F
function

TestAuth_ValidToken

Parameters

app/web/auth_test.go:46-62
func TestAuth_ValidToken(t *testing.T)

{
	s := New()
	s.Use(Auth(authTestSecret))
	s.MapGet("/api", func(ctx *Context) error {
		return ctx.JSON(200, map[string]string{"ok": "yes"})
	})

	token, _ := auth.SignToken(auth.Payload{Sub: "user1", Exp: 9999999999}, authTestSecret)
	req := httptest.NewRequest("GET", "/api", nil)
	req.Header.Set("Authorization", "Bearer "+token)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	if w.Code != 200 {
		t.Errorf("status: got %d, want 200", w.Code)
	}
}
F
function

TestAuth_RejectsShortSecretAtConfiguration

Parameters

app/web/auth_test.go:64-71
func TestAuth_RejectsShortSecretAtConfiguration(t *testing.T)

{
	defer func() {
		if recover() == nil {
			t.Fatal("Auth() accepted a short HMAC secret")
		}
	}()
	Auth([]byte("short"))
}
F
function

TestRunDoctorEnabled

Parameters

app/web/doctor_enabled_test.go:7-19
func TestRunDoctorEnabled(t *testing.T)

{
	t.Setenv("FOUNDATION_DOCTOR", "fail")

	s := New()
	s.MapGet("/ping", func(ctx *Context) error {
		ctx.String(200, "pong")
		return nil
	})

	if err := s.runDoctor(); err != nil {
		t.Fatalf("runDoctor() error = %v", err)
	}
}
F
function

TestRunDoctorEnabledFails

Parameters

app/web/doctor_enabled_test.go:21-28
func TestRunDoctorEnabledFails(t *testing.T)

{
	t.Setenv("FOUNDATION_DOCTOR", "fail")

	s := New()
	if err := s.runDoctor(); err == nil {
		t.Fatal("runDoctor() error = nil, want error")
	}
}
I
interface

Handler

Handler is the interface for declarative struct-tagged endpoints.

app/web/handler.go:17-19
type Handler interface

Methods

Handle
Method

Parameters

Returns

any
error
func Handle(...)
S
struct

HandlerDefinition

HandlerDefinition describes a handler without runtime metadata discovery.

app/web/handler.go:22-26
type HandlerDefinition struct

Fields

Name Type Description
Method string
Path string
New func() Handler
F
function

DefinitionFromHandler

DefinitionFromHandler describes a reflection-based prototype for batch validation.

Parameters

prototype

Returns

app/web/handler.go:46-76
func DefinitionFromHandler(prototype Handler) (HandlerDefinition, error)

{
	value := reflect.ValueOf(prototype)
	if !value.IsValid() || value.Kind() != reflect.Ptr || value.IsNil() || value.Elem().Kind() != reflect.Struct {
		return HandlerDefinition{}, fmt.Errorf("web: handler prototype must be a non-nil pointer to a struct")
	}
	typ := value.Elem().Type()
	var method, path string
	for i := 0; i < typ.NumField(); i++ {
		sf := typ.Field(i)
		if m := sf.Tag.Get("method"); m != "" {
			method = m
		}
		if p := sf.Tag.Get("path"); p != "" {
			path = p
		}
	}

	if method == "" || path == "" {
		return HandlerDefinition{}, fmt.Errorf("web: handler %T must declare method and path tags", prototype)
	}

	return HandlerDefinition{
		Method: method,
		Path:   path,
		New: func() Handler {
			newVal := reflect.New(typ)
			newVal.Elem().Set(value.Elem())
			return newVal.Interface().(Handler)
		},
	}, nil
}
S
struct

preparedDefinition

app/web/handler.go:118-121
type preparedDefinition struct

Fields

Name Type Description
meta *handlerMeta
handler HandlerFunc
F
function

validatePreparedDefinitions

Parameters

routes
prepared

Returns

error
app/web/handler.go:165-178
func validatePreparedDefinitions(routes []route, prepared []preparedDefinition) error

{
	tree := newRadixTree()
	for _, registered := range routes {
		if err := tree.insert(registered.method, registered.path, registered.handler); err != nil {
			return err
		}
	}
	for _, item := range prepared {
		if err := tree.insert(item.meta.method, item.meta.path, item.handler); err != nil {
			return err
		}
	}
	return nil
}
S
struct

handlerMeta

app/web/handler.go:180-187
type handlerMeta struct

Fields

Name Type Description
new func() Handler
name string
method string
path string
container *di.Container
jsonBody bool
F
function

hasJSONBody

Parameters

handler

Returns

bool
app/web/handler.go:241-252
func hasJSONBody(handler Handler) bool

{
	typ := reflect.TypeOf(handler)
	if typ.Kind() == reflect.Ptr {
		typ = typ.Elem()
	}
	for i := 0; i < typ.NumField(); i++ {
		if typ.Field(i).Tag.Get("body") == "json" {
			return true
		}
	}
	return false
}
F
function

isJSONContentType

Parameters

value
string

Returns

bool
app/web/handler.go:254-260
func isJSONContentType(value string) bool

{
	contentType, _, err := mime.ParseMediaType(value)
	if err != nil {
		return false
	}
	return contentType == "application/json" || strings.HasSuffix(contentType, "+json")
}
S
struct

clientRateLimiter

app/web/ratelimit.go:19-23
type clientRateLimiter struct

Fields

Name Type Description
key string
limiter *resiliency.RateLimiter
lastSeen time.Time
S
struct

clientRateLimitStore

app/web/ratelimit.go:25-32
type clientRateLimitStore struct

Methods

forClient
Method

Parameters

key string
now time.Time

Returns

func (*clientRateLimitStore) forClient(key string, now time.Time) (*resiliency.RateLimiter, error)
{
	s.mu.Lock()
	defer s.mu.Unlock()

	if s.lastCleanup.IsZero() || now.Sub(s.lastCleanup) >= rateLimitClientTTL {
		s.removeExpired(now)
		s.lastCleanup = now
	}
	if element, ok := s.clients[key]; ok {
		entry := element.Value.(*clientRateLimiter)
		entry.lastSeen = now
		s.recency.MoveToFront(element)
		return entry.limiter, nil
	}
	if len(s.clients) >= rateLimitMaxClients {
		s.removeElement(s.recency.Back())
	}
	limiter, err := resiliency.NewRateLimiter(s.rate, s.burst)
	if err != nil {
		return nil, err
	}
	entry := &clientRateLimiter{key: key, limiter: limiter, lastSeen: now}
	s.clients[key] = s.recency.PushFront(entry)
	return limiter, nil
}
removeExpired
Method

Parameters

now time.Time
func (*clientRateLimitStore) removeExpired(now time.Time)
{
	for element := s.recency.Back(); element != nil; {
		entry := element.Value.(*clientRateLimiter)
		if now.Sub(entry.lastSeen) <= rateLimitClientTTL {
			return
		}
		previous := element.Prev()
		s.removeElement(element)
		element = previous
	}
}
removeElement
Method

Parameters

element *list.Element
func (*clientRateLimitStore) removeElement(element *list.Element)
{
	if element == nil {
		return
	}
	entry := element.Value.(*clientRateLimiter)
	delete(s.clients, entry.key)
	s.recency.Remove(element)
}

Fields

Name Type Description
mu sync.Mutex
clients map[string]*list.Element
recency *list.List
rate int
burst int
lastCleanup time.Time
F
function

RateLimit

RateLimit returns nonblocking per-client rate limit middleware.

Parameters

rate
int
burst
int

Returns

error
app/web/ratelimit.go:35-57
func RateLimit(rate, burst int) (Middleware, error)

{
	if _, err := resiliency.NewRateLimiter(rate, burst); err != nil {
		return nil, err
	}
	store := &clientRateLimitStore{
		clients: make(map[string]*list.Element),
		recency: list.New(),
		rate:    rate,
		burst:   burst,
	}
	return func(next HandlerFunc) HandlerFunc {
		return func(ctx *Context) error {
			limiter, err := store.forClient(clientIdentity(ctx.Request), time.Now())
			if err != nil {
				return fmt.Errorf("web: rate limiter: %w", err)
			}
			if !limiter.Allow() {
				return Error(http.StatusTooManyRequests, "rate limit exceeded")
			}
			return next(ctx)
		}
	}, nil
}
F
function

clientIdentity

Parameters

request

Returns

string
app/web/ratelimit.go:106-115
func clientIdentity(request *http.Request) string

{
	host, _, err := net.SplitHostPort(request.RemoteAddr)
	if err == nil && host != "" {
		return host
	}
	if request.RemoteAddr != "" {
		return request.RemoteAddr
	}
	return "unknown"
}