Documentation
¶
Overview ¶
Package router is the Līdza control plane's HTTP router: the API routes an application registers, plus the built-in ones every app serves.
Index ¶
- Constants
- func Builtins() []string
- func Error(w http.ResponseWriter, status int, msg string)
- func ErrorCode(status int, code, format string, a ...any) error
- func Errorf(status int, format string, a ...any) error
- func JSON(w http.ResponseWriter, status int, v any)
- func NotFound(what string) error
- func Route[In, Out any](r *Router, pattern string, h Handler[In, Out], mw ...middleware.Middleware)
- func Stream[In, Event any](r *Router, pattern string, h StreamHandler[In, Event], ...)
- func UploadLimit(n int64) middleware.Middleware
- func WriteError(w http.ResponseWriter, req *http.Request, err error)
- type File
- type HTTPError
- type Handler
- type Health
- type Mount
- type None
- type Request
- type Router
- func (r *Router) Group(prefix string, mw ...middleware.Middleware) *Router
- func (r *Router) Handle(pattern string, h http.Handler)
- func (r *Router) HandleFunc(pattern string, h http.HandlerFunc)
- func (r *Router) Handler() http.Handler
- func (r *Router) Mount(prefix string, h http.Handler)
- func (r *Router) Mounts() []Mount
- func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request)
- func (r *Router) Use(mw ...middleware.Middleware)
- type StreamHandler
Constants ¶
const APIPrefix = "/api/"
APIPrefix is the path prefix reserved for the Go control plane. Everything under it is handled by the Router; everything else belongs to the frontend.
const MaxBodyBytes = 1 << 20
MaxBodyBytes is the largest JSON request body a typed handler accepts.
const MaxUploadBytes = 32 << 20
MaxUploadBytes is the largest body an upload route (In = File) accepts unless UploadLimit sets another.
Variables ¶
This section is empty.
Functions ¶
func Builtins ¶
func Builtins() []string
Builtins lists the patterns of the routes every app serves.
func Error ¶
func Error(w http.ResponseWriter, status int, msg string)
Error writes a JSON error body: {"error": msg}.
func ErrorCode ¶
ErrorCode is Errorf with a code the client can switch on: the reply is {"error": message, "code": code}.
func JSON ¶
func JSON(w http.ResponseWriter, status int, v any)
JSON writes v as a JSON response with the given status code.
func Route ¶
func Route[In, Out any](r *Router, pattern string, h Handler[In, Out], mw ...middleware.Middleware)
Route registers a typed handler at a ServeMux pattern. The request body is decoded from JSON into In (up to MaxBodyBytes) for methods that carry one; when In implements validate.Validator the body is validated and a failure replies 422 with the field errors. Out is encoded as JSON with status 200 (or the status set on the request); None replies 204. Errors: *HTTPError sends its status, *validate.Errors sends 422, anything else sends 500 and is logged, its text never reaching the client.
With In = File the route is an upload: the body is the file itself, not JSON, handed to the handler unread (see File).
func Stream ¶
func Stream[In, Event any](r *Router, pattern string, h StreamHandler[In, Event], mw ...middleware.Middleware)
Stream registers a streaming handler at a ServeMux pattern. The body is decoded and validated as Route does it. The reply is server-sent events (text/event-stream): each send is one event whose data is the Event as JSON, and the stream closes with an "end" event once the handler returns nil. The generated clients read it as an async iterable (TS) or a Stream (Dart):
router.Stream(r, "POST /api/v1/summaries", summarize)
func summarize(ctx context.Context, req *router.Request[schema.SummaryInput], send func(string) error) error {
_, err := llm.From(ctx).Stream(ctx, llm.Request{
Messages: []llm.Message{{Role: llm.User, Content: req.Body.Prompt}},
}, send)
return err
}
An error the handler returns before its first send replies as Route does, with the status and a JSON body. After it the headers are gone: the error is sent as an "error" event, its data the same body plus "status", and the stream closes. send fails once the client has gone or the request's deadline (App.StreamTimeout for a client that accepts text/event-stream) has passed; return then. send may be called from several goroutines, not after the handler returns.
func UploadLimit ¶
func UploadLimit(n int64) middleware.Middleware
UploadLimit sets the largest body, in bytes, the upload routes it wraps accept; MaxUploadBytes otherwise.
func WriteError ¶
func WriteError(w http.ResponseWriter, req *http.Request, err error)
WriteError answers a raw handler's error the way typed handlers do: an HTTPError with its status and message, validation errors as a 422 with fields, anything else as a 500 whose text stays on the server.
Types ¶
type File ¶
type File struct {
// Body reads the uploaded bytes, bounded by the route's limit.
Body io.Reader
// ContentType is the request's Content-Type, application/octet-stream
// when it has none.
ContentType string
// Name is the file name the client sent in Content-Disposition (the
// generated client sends a File's name); empty otherwise. It is the
// client's word: never use it as a path or a storage key.
Name string
// Size is the Content-Length, or -1 when the client did not send one.
Size int64
}
File is the In type of an upload route: the request body is the file itself, sent raw (not JSON, not multipart form data). The generated client takes a File or Blob for it, with the path parameters:
router.Route(r, "PUT /api/v1/posts/{id}/image", uploadImage, router.UploadLimit(10<<20))
func uploadImage(ctx context.Context, req *router.Request[router.File]) (storage.Object, error) {
return storage.From(ctx).Put(ctx, "posts/"+req.Param("id")+"/image", req.Body.Body,
storage.PutOptions{ContentType: req.Body.ContentType})
}
await api.uploadImage({ id }, file, { onProgress: (sent, total) => ... })
A body over the limit replies 413: at once when Content-Length says so, or when the handler returns the read error.
type HTTPError ¶
type HTTPError struct {
Status int
Message string
// Code, when set, names the error for programs ("wrong_password"),
// so a client tells apart two errors with one status without reading
// the message. It is sent as "code" beside "error".
Code string
}
HTTPError is an error with a status code. Route sends {"error": Message} with that status; the message is what the client sees.
type Handler ¶
Handler is a typed handler: JSON in, JSON out, errors mapped to status codes by Route.
type Health ¶
type Health struct {
Status string `json:"status"`
Version string `json:"version"`
Time time.Time `json:"time"`
}
Health is the response body of GET /api/v1/health.
type Mount ¶
Mount is a handler served outside /api, at a path prefix, before the frontend: the admin pages, a webhook endpoint, a file server.
type None ¶
type None struct{}
None is the In type of a handler without a request body and the Out type of one that replies 204 No Content.
type Request ¶
type Request[In any] struct { // Body is the decoded JSON body; the zero value for None or when the // method carries no body. Body In // Raw is the underlying request, for headers, path values and query. Raw *http.Request // contains filtered or unexported fields }
Request is what a typed handler receives.
type Router ¶
type Router struct {
// contains filtered or unexported fields
}
Router registers and serves API handlers. It wraps net/http's ServeMux, so patterns take the "METHOD /path/{param}" form.
func (*Router) Group ¶
func (r *Router) Group(prefix string, mw ...middleware.Middleware) *Router
Group returns a router for the routes at prefix and below it ("/api/v1/notes" covers "/api/v1/notes" and "/api/v1/notes/{id}") with middleware of its own, run after the parent's. Routes registered on the group use full patterns, as on the parent:
notes := r.Group("/api/v1/notes", auth.Require())
router.Route(notes, "GET /api/v1/notes", listNotes)
Every route lands on the root router's ServeMux with its full pattern, the group's middleware wrapped around that route alone. ServeMux precedence therefore holds across groups: the more specific pattern wins wherever it was registered ("GET /api/v1/notes/recent" in a group over "GET /api/v1/notes/{id}" on the parent), and two conflicting patterns panic at registration. A path no route matches is a 404 (a 405 with Allow for a wrong method) without the group's middleware.
The prefix may hold wildcards; the group's patterns repeat them and the handlers read their values:
post := r.Group("/api/v1/posts/{id}", auth.Require())
router.Route(post, "GET /api/v1/posts/{id}/tags", listTags) // req.Param("id")
A pattern outside the prefix panics.
func (*Router) Handle ¶
Handle registers a handler for a ServeMux pattern. On a group the pattern's path must lie at the group's prefix or below it.
func (*Router) HandleFunc ¶
func (r *Router) HandleFunc(pattern string, h http.HandlerFunc)
HandleFunc registers a handler function for a ServeMux pattern.
func (*Router) Handler ¶
Handler returns the router with its middleware applied. A group serves through the root router: its Handler is the root's.
func (*Router) Mount ¶
Mount serves h at prefix and below it, outside /api, ahead of the frontend ("/admin/" for the admin pages). The app's middleware and services apply; the router's own do not, nor a group's.
func (*Router) ServeHTTP ¶
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request)
ServeHTTP implements http.Handler.
func (*Router) Use ¶
func (r *Router) Use(mw ...middleware.Middleware)
Use adds middleware around every route, outermost first. Call it before the router serves its first request; later calls have no effect.