srv
packageAPI reference for the srv
package.
Imports
(21)net/http
STD
strings
INT
github.com/mirkobrombin/go-foundation/pkg/auth
STD
net/http/httptest
STD
testing
STD
regexp
STD
strconv
STD
sync
INT
github.com/mirkobrombin/go-foundation/pkg/resiliency
STD
context
STD
encoding/json
STD
errors
STD
fmt
STD
reflect
STD
runtime/debug
STD
time
INT
github.com/mirkobrombin/go-foundation/pkg/options
INT
github.com/mirkobrombin/go-foundation/pkg/validation
STD
io
INT
github.com/mirkobrombin/go-foundation/pkg/bind
INT
github.com/mirkobrombin/go-foundation/pkg/di
Auth
Auth returns middleware that validates a Bearer token using auth.VerifyToken.
A valid token is attached to the context via auth.Payload.
Parameters
Returns
func Auth(secret []byte) Middleware
{
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": err.Error()})
}
ctx.Set("auth.payload", payload)
return next(ctx)
}
}
}
Uses
TestAuth_MissingHeader
Parameters
func TestAuth_MissingHeader(t *testing.T)
{
s := New()
s.Use(Auth([]byte("secret")))
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)
}
}
TestAuth_InvalidToken
Parameters
func TestAuth_InvalidToken(t *testing.T)
{
s := New()
s.Use(Auth([]byte("secret")))
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)
}
}
TestAuth_ValidToken
Parameters
func TestAuth_ValidToken(t *testing.T)
{
s := New()
s.Use(Auth([]byte("secret")))
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}, []byte("secret"))
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)
}
}
methodHandlers
type methodHandlers map[string]HandlerFunc
paramConstraint
type paramConstraint struct
Fields
| Name | Type | Description |
|---|---|---|
| name | string | |
| validate | func(string) bool |
radixNode
type radixNode struct
Fields
| Name | Type | Description |
|---|---|---|
| children | []*radixNode | |
| segment | string | |
| isParam | bool | |
| isCatchAll | bool | |
| paramName | string | |
| constraints | []paramConstraint | |
| handlers | methodHandlers |
Uses
radixTree
type radixTree struct
Methods
Parameters
func (*radixTree) insert(method, path string, handler HandlerFunc)
{
t.mu.Lock()
defer t.mu.Unlock()
segments := splitPath(path)
if len(segments) > 0 && isCatchAll(segments[len(segments)-1]) {
segments = segments[:len(segments)-1]
node := t.walkOrCreate(t.root, segments)
paramName, _ := parseCatchAll(path)
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
}
node := t.walkOrCreate(t.root, segments)
if node.handlers == nil {
node.handlers = make(methodHandlers)
}
node.handlers[method] = handler
}
func (*radixTree) walkOrCreate(node *radixNode, segments []string) *radixNode
{
for _, seg := range segments {
if isParam(seg) {
name, constraints := parseParamConstraints(seg)
node = findOrCreateParam(node, name, constraints)
} else {
node = findOrCreateStatic(node, seg)
}
}
return node
}
Parameters
Returns
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)
node := t.root
segmentIdx := 0
for segmentIdx < len(segments) {
seg := segments[segmentIdx]
var next *radixNode
for _, child := range node.children {
if !child.isParam && !child.isCatchAll && child.segment == seg {
next = child
break
}
}
if next == nil {
for _, child := range node.children {
if child.isCatchAll {
remaining := strings.Join(segments[segmentIdx:], "/")
params[child.paramName] = remaining
handler, ok := child.handlers[method]
if !ok {
return nil, nil
}
return handler, params
}
if child.isParam {
if len(child.constraints) > 0 {
allValid := true
for _, c := range child.constraints {
if !c.validate(seg) {
allValid = false
break
}
}
if !allValid {
return nil, nil
}
}
params[child.paramName] = seg
next = child
break
}
}
}
if next == nil {
return nil, nil
}
node = next
segmentIdx++
}
if node.handlers == nil {
for _, child := range node.children {
if child.isCatchAll {
params[child.paramName] = ""
handler, ok := child.handlers[method]
if !ok {
return nil, nil
}
return handler, params
}
}
return nil, nil
}
handler, ok := node.handlers[method]
if !ok {
return nil, nil
}
return handler, params
}
Fields
| Name | Type | Description |
|---|---|---|
| root | *radixNode | |
| mu | sync.RWMutex |
newRadixTree
Returns
func newRadixTree() *radixTree
{
return &radixTree{
root: &radixNode{children: make([]*radixNode, 0)},
}
}
findOrCreateStatic
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
}
findOrCreateParam
Parameters
Returns
func findOrCreateParam(parent *radixNode, paramName string, constraints []paramConstraint) *radixNode
{
for _, child := range parent.children {
if child.isParam && child.paramName == paramName {
if len(constraints) > 0 && len(child.constraints) == 0 {
child.constraints = constraints
}
return child
}
}
node := &radixNode{
isParam: true,
paramName: paramName,
constraints: constraints,
children: make([]*radixNode, 0),
}
parent.children = append(parent.children, node)
return node
}
splitPath
Parameters
Returns
func splitPath(path string) []string
{
trimmed := strings.Trim(path, "/")
if trimmed == "" {
return nil
}
return strings.Split(trimmed, "/")
}
isParam
Parameters
Returns
func isParam(seg string) bool
{
return len(seg) > 2 && seg[0] == '{' && seg[len(seg)-1] == '}'
}
isCatchAll
Parameters
Returns
func isCatchAll(seg string) bool
{
return len(seg) > 3 && seg[0] == '{' && seg[1] == '*' && seg[len(seg)-1] == '}'
}
parseCatchAll
Parameters
Returns
func parseCatchAll(path string) (string, string)
{
parts := strings.Split(strings.Trim(path, "/"), "/")
last := parts[len(parts)-1]
if isCatchAll(last) {
return last[2 : len(last)-1], ""
}
return "", ""
}
parseParamConstraints
Parameters
Returns
func parseParamConstraints(seg string) (string, []paramConstraint)
{
inner := seg[1 : len(seg)-1]
colonIdx := strings.Index(inner, ":")
if colonIdx < 0 {
return inner, nil
}
name := inner[:colonIdx]
constraintStr := inner[colonIdx+1:]
var constraints []paramConstraint
for _, c := range strings.Split(constraintStr, ",") {
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]
constraints = append(constraints, paramConstraint{
name: c,
validate: isRegexConstraint(pattern),
})
}
}
}
return name, constraints
}
isIntConstraint
Parameters
Returns
func isIntConstraint(s string) bool
{
_, err := strconv.Atoi(s)
return err == nil
}
isAlphaConstraint
Parameters
Returns
func isAlphaConstraint(s string) bool
{
for _, r := range s {
if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')) {
return false
}
}
return true
}
isRegexConstraint
Parameters
Returns
func isRegexConstraint(pattern string) func(string) bool
{
re := regexp.MustCompile(pattern)
return func(s string) bool {
return re.MatchString(s)
}
}
RateLimit
RateLimit returns middleware that limits requests per client using a token bucket.
Parameters
Returns
func RateLimit(rate, burst int) Middleware
{
rl := resiliency.NewRateLimiter(rate, burst)
return func(next HandlerFunc) HandlerFunc {
return func(ctx *Context) error {
if err := rl.Wait(ctx.Ctx); err != nil {
return ctx.JSON(http.StatusTooManyRequests, map[string]string{"error": "rate limit exceeded"})
}
return next(ctx)
}
}
}
Uses
Context
Context holds the request state for a single HTTP request.
type Context struct
Methods
Set stores a key-value pair in the request context.
Parameters
func (*Context) Set(key string, val any)
{
if c.values == nil {
c.values = make(map[string]any)
}
c.values[key] = val
}
Get retrieves a value from the request context by key.
Parameters
Returns
func (*Context) Get(key string) (any, bool)
{
v, ok := c.values[key]
return v, ok
}
JSON writes a JSON response with the given status code.
Parameters
Returns
func (*Context) JSON(code int, v any) error
{
c.Response.Header().Set("Content-Type", "application/json; charset=utf-8")
c.Response.WriteHeader(code)
return json.NewEncoder(c.Response).Encode(v)
}
String writes a plain text response with the given status code.
Parameters
func (*Context) String(code int, s string)
{
c.Response.Header().Set("Content-Type", "text/plain; charset=utf-8")
c.Response.WriteHeader(code)
c.Response.Write([]byte(s))
}
Bind decodes the request body into v based on the Content-Type header.
Parameters
Returns
func (*Context) Bind(v any) error
{
ct := c.Request.Header.Get("Content-Type")
if strings.Contains(ct, "application/json") {
return json.NewDecoder(c.Request.Body).Decode(v)
}
if strings.Contains(ct, "application/x-www-form-urlencoded") {
if err := c.Request.ParseForm(); err != nil {
return fmt.Errorf("srv: cannot parse form: %w", err)
}
return bindForm(c.Request.Form, v)
}
return fmt.Errorf("srv: unsupported content type: %s", ct)
}
Validate runs validation rules on the target object and returns the collected validation errors. Returns nil if there are no errors.
Parameters
Returns
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
Returns
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 |
bindForm
Parameters
Returns
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("srv: 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 := elem.Type().Field(i).Name
jsonTag := elem.Type().Field(i).Tag.Get("json")
if jsonTag != "" {
parts := strings.Split(jsonTag, ",")
if parts[0] != "" && parts[0] != "-" {
fieldName = parts[0]
}
}
if vals, ok := form[fieldName]; ok && len(vals) > 0 {
setFieldValue(field, vals[0])
}
}
return nil
}
setFieldValue
Parameters
func setFieldValue(field reflect.Value, value string)
{
switch field.Kind() {
case reflect.String:
field.SetString(value)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if n, err := strconv.ParseInt(value, 10, 64); err == nil {
field.SetInt(n)
}
case reflect.Float32, reflect.Float64:
if n, err := strconv.ParseFloat(value, 64); err == nil {
field.SetFloat(n)
}
case reflect.Bool:
if b, err := strconv.ParseBool(value); err == nil {
field.SetBool(b)
}
}
}
HandlerFunc
HandlerFunc is the handler signature for srv routes.
type HandlerFunc func(*Context) error
Middleware
Middleware wraps a HandlerFunc, returning a new HandlerFunc.
type Middleware func(HandlerFunc) HandlerFunc
route
type route struct
Fields
| Name | Type | Description |
|---|---|---|
| method | string | |
| path | string | |
| handler | HandlerFunc |
Uses
Server
Server is a minimal API HTTP server with routing and middleware support.
type Server struct
Methods
Group creates a route group with a common prefix and optional middleware.
Parameters
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 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 registers a GET route at the given path.
Parameters
func (*Server) MapGet(path string, handler HandlerFunc, mw ...Middleware)
{
s.addRoute("GET", path, handler, mw...)
}
MapPost registers a POST route at the given path.
Parameters
func (*Server) MapPost(path string, handler HandlerFunc, mw ...Middleware)
{
s.addRoute("POST", path, handler, mw...)
}
MapPut registers a PUT route at the given path.
Parameters
func (*Server) MapPut(path string, handler HandlerFunc, mw ...Middleware)
{
s.addRoute("PUT", path, handler, mw...)
}
MapDelete registers a DELETE route at the given path.
Parameters
func (*Server) MapDelete(path string, handler HandlerFunc, mw ...Middleware)
{
s.addRoute("DELETE", path, handler, mw...)
}
Parameters
func (*Server) addRoute(method, path string, handler HandlerFunc, mw ...Middleware)
{
chained := chainMiddleware(handler, mw...)
s.mu.Lock()
s.routes = append(s.routes, route{method: method, path: path, handler: chained})
s.tree.insert(method, path, chained)
s.mu.Unlock()
}
ServeHTTP implements http.Handler, dispatching requests through global middleware and routes.
Parameters
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 {
s.serveRoutesWithContext(c)
return nil
}
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": err.Error()})
}
}
Parameters
func (*Server) serveRoutesWithContext(ctx *Context)
{
handler, params := s.tree.lookup(ctx.Request.Method, ctx.Request.URL.Path)
if handler == nil {
ctx.Response.Header().Set("Content-Type", "application/json; charset=utf-8")
ctx.Response.WriteHeader(http.StatusNotFound)
json.NewEncoder(ctx.Response).Encode(map[string]string{"error": "not found"})
return
}
ctx.Params = params
if err := handler(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": err.Error()})
}
}
ListenAndServe starts the server on the given address.
Parameters
Returns
func (*Server) ListenAndServe(addr string) error
{
s.mu.Lock()
s.server = &http.Server{Addr: addr, Handler: s}
s.mu.Unlock()
return s.server.ListenAndServe()
}
ListenAndServeTLS starts the server with TLS on the given address.
Parameters
Returns
func (*Server) ListenAndServeTLS(addr, certFile, keyFile string) error
{
s.mu.Lock()
s.server = &http.Server{Addr: addr, Handler: s}
s.mu.Unlock()
return s.server.ListenAndServeTLS(certFile, keyFile)
}
Shutdown gracefully shuts down the server.
Parameters
Returns
func (*Server) Shutdown(ctx context.Context) error
{
s.mu.RLock()
srv := s.server
s.mu.RUnlock()
if srv == nil {
return fmt.Errorf("srv: server not started")
}
return srv.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
func (*Server) RegisterHandler(prototype Handler, container *di.Container)
{
meta := &handlerMeta{
prototype: reflect.TypeOf(prototype).Elem(),
container: container,
}
typ := meta.prototype
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 == "" {
panic("RegisterHandler: struct must have method and path struct tags")
}
meta.method = method
meta.path = path
handler := s.buildHandler(meta)
s.tree.insert(method, path, handler)
}
Parameters
Returns
func (*Server) buildHandler(meta *handlerMeta) HandlerFunc
{
return func(ctx *Context) error {
newVal := reflect.New(meta.prototype)
if meta.container != nil {
meta.container.Inject(newVal.Interface())
}
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)
b.Bind(newVal.Interface())
if ctx.Request.Body != nil {
if ct := ctx.Request.Header.Get("Content-Type"); ct == "application/json" {
body, err := io.ReadAll(ctx.Request.Body)
if err == nil && len(body) > 0 {
_ = bind.New().BindJSON(newVal.Interface(), body)
}
}
}
result, err := newVal.Interface().(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 |
Option
Option configures a Server.
type Option options.Option[Server]
New
New creates a new Server with optional configuration.
Parameters
Returns
func New(opts ...Option) *Server
{
s := &Server{
tree: newRadixTree(),
}
for _, opt := range opts {
opt(s)
}
return s
}
group
type group struct
Methods
MapGet registers a GET route in the group.
Parameters
func (*group) MapGet(path string, handler HandlerFunc)
{
g.server.MapGet(g.prefix+path, handler, g.middleware...)
}
MapPost registers a POST route in the group.
Parameters
func (*group) MapPost(path string, handler HandlerFunc)
{
g.server.MapPost(g.prefix+path, handler, g.middleware...)
}
MapPut registers a PUT route in the group.
Parameters
func (*group) MapPut(path string, handler HandlerFunc)
{
g.server.MapPut(g.prefix+path, handler, g.middleware...)
}
MapDelete registers a DELETE route in the group.
Parameters
func (*group) MapDelete(path string, handler HandlerFunc)
{
g.server.MapDelete(g.prefix+path, handler, g.middleware...)
}
Fields
| Name | Type | Description |
|---|---|---|
| prefix | string | |
| middleware | []Middleware | |
| server | *Server |
chainMiddleware
Parameters
Returns
func chainMiddleware(h HandlerFunc, mw ...Middleware) HandlerFunc
{
for i := len(mw) - 1; i >= 0; i-- {
h = mw[i](h)
}
return h
}
matchRoute
Parameters
Returns
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
}
JSON
JSON writes a JSON response using the context.
Parameters
Returns
func JSON(ctx *Context, code int, v any) error
{
return ctx.JSON(code, v)
}
Error
Error creates an HTTP error with a status code and message.
Parameters
Returns
func Error(code int, msg string) error
{
return &httpError{Code: code, Message: msg}
}
httpError
type httpError struct
Methods
Fields
| Name | Type | Description |
|---|---|---|
| Code | int | |
| Message | string |
panicError
type panicError struct
Methods
Returns
func (*panicError) Error() string
{
return fmt.Sprintf("panic: %v\n%s", e.recovered, e.stack)
}
Returns
func (*panicError) HTTPCode() int
{
return http.StatusInternalServerError
}
Returns
func (*panicError) Unwrap() error
{
if err, ok := e.recovered.(error); ok {
return err
}
return nil
}
Fields
| Name | Type | Description |
|---|---|---|
| recovered | any | |
| stack | string |
Recovery
Recovery returns middleware that catches panics and converts them to errors with stack traces.
Returns
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)
}
}
}
Uses
Logger
Logger returns middleware that logs request method, path, duration, and status.
Returns
func Logger() Middleware
{
return func(next HandlerFunc) HandlerFunc {
return func(ctx *Context) error {
start := time.Now()
err := next(ctx)
fmt.Printf("%s %s %v %d\n", ctx.Request.Method, ctx.Request.URL.Path, time.Since(start), statusFromError(err))
return err
}
}
}
Uses
CORS
CORS returns middleware that sets CORS headers. Preflight OPTIONS requests get 204.
Parameters
Returns
func CORS(allowOrigins ...string) Middleware
{
origins := "*"
if len(allowOrigins) > 0 {
origins = strings.Join(allowOrigins, ", ")
}
return func(next HandlerFunc) HandlerFunc {
return func(ctx *Context) error {
ctx.Response.Header().Set("Access-Control-Allow-Origin", origins)
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)
}
}
}
Uses
RequestID
RequestID returns middleware that sets and propagates X-Request-ID.
Returns
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)
}
}
}
Uses
Compress
Compress returns middleware that sets gzip content encoding.
Returns
func Compress() Middleware
{
return func(next HandlerFunc) HandlerFunc {
return func(ctx *Context) error {
ctx.Response.Header().Set("Content-Encoding", "gzip")
return next(ctx)
}
}
}
Uses
HealthEndpoint
HealthEndpoint returns a handler that reports health status based on the checker function.
Parameters
Returns
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", "error": err.Error()})
}
return ctx.JSON(http.StatusOK, map[string]string{"status": "healthy"})
}
}
Uses
statusFromError
Parameters
Returns
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
}
Handler
Handler is the interface for declarative struct-tagged endpoints.
type Handler interface
Methods
handlerMeta
type handlerMeta struct
Fields
| Name | Type | Description |
|---|---|---|
| prototype | reflect.Type | |
| method | string | |
| path | string | |
| container | *di.Container |
PingEndpoint
type PingEndpoint struct
Methods
Parameters
Returns
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" |
pingResponse
type pingResponse struct
Fields
| Name | Type | Description |
|---|---|---|
| Message | []string | json:"message" |
TestRegisterHandler_Basic
Parameters
func TestRegisterHandler_Basic(t *testing.T)
{
s := New()
b := di.NewBuilder()
container, _ := b.Build()
s.RegisterHandler(&PingEndpoint{}, container)
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))
}
}
TestRegisterHandler_WithQuery
Parameters
func TestRegisterHandler_WithQuery(t *testing.T)
{
s := New()
b := di.NewBuilder()
container, _ := b.Build()
s.RegisterHandler(&PingEndpoint{}, container)
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))
}
}
DiEndpoint
type DiEndpoint struct
Methods
Parameters
Returns
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 |
TestRegisterHandler_WithDI
Parameters
func TestRegisterHandler_WithDI(t *testing.T)
{
s := New()
b := di.NewBuilder()
b.Provide("Greet", "custom-greeting")
container, _ := b.Build()
s.RegisterHandler(&DiEndpoint{}, container)
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)
}
}
NoReturnEndpoint
type NoReturnEndpoint struct
Methods
Parameters
Returns
func (*NoReturnEndpoint) Handle(_ context.Context) (any, error)
{
return nil, nil
}
Fields
| Name | Type | Description |
|---|---|---|
| Meta | struct{} | method:"DELETE" path:"/item" |
TestRegisterHandler_NilReturn
Parameters
func TestRegisterHandler_NilReturn(t *testing.T)
{
s := New()
b := di.NewBuilder()
container, _ := b.Build()
s.RegisterHandler(&NoReturnEndpoint{}, container)
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)
}
}
TestServer_MapGet
Parameters
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")
}
}
TestServer_Params
Parameters
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")
}
}
TestServer_NotFound
Parameters
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)
}
}
TestServer_Middleware
Parameters
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"])
}
}
TestServer_Group
Parameters
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)
}
}
TestServer_CORS
Parameters
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"))
}
}
TestServer_Bind
Parameters
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)
}
}
TestHealthEndpoint
Parameters
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)
}
}
TestServer_IntConstraint
Parameters
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)
}
}
TestServer_AlphaConstraint
Parameters
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)
}
}
TestServer_CatchAll
Parameters
func TestServer_CatchAll(t *testing.T)
{
s := New()
s.MapGet("/static/{*filepath}", func(ctx *Context) error {
return ctx.JSON(200, map[string]string{"path": ctx.Params["filepath"]})
})
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")
}
}
TestServer_MethodMultiplex
Parameters
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)
}
}
TestServer_RegexConstraint
Parameters
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)
}
}