web API

web

package

API reference for the web package.

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")
}
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
}
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"
}
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

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)
}
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))
}

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

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)
	}
}
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)
}
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})
}

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

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)
	}
}
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")
	}
}
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"))
}
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

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)
		}
	}
}