Package jsonhandler
Overview
Package jsonhandler builds an HTTP handler for a JSON endpoint from one operation: a func(context.Context, Params) (Result, error).
The handler maps between the wire and the operation, and nothing else:
- It negotiates the response's media type from Accept, or responds 406.
- It decodes the body as JSON into the wire request, checking its media type and content coding (415) and size (413). A gzip body is decoded, and its decoded size is what counts.
- It converts the wire request to the operation's params with its Params method (400).
- It calls the operation, and maps its declared errors to statuses (else 500).
- It converts the result to the wire response and encodes it.
Every error is written as an RFC 9457 problem. Wire an endpoint by naming its wire request type; the rest is inferred from the operation and the response constructor:
mux.Handle("POST /users", jsonhandler.New[createUserRequest](registerUser, newCreateUserResponse).
WithStatus(http.StatusCreated).
WithErrors(problem.ErrorStatus{Err: ErrUserExists, Status: rfc9110.Conflict()}).
Handler())Index
- Constants
- Variables
type Builderfunc Newfunc (Builder[Request, Params, Result, Response]) Handlerfunc (Builder[Request, Params, Result, Response]) WithCacheControlfunc (Builder[Request, Params, Result, Response]) WithErrorsfunc (Builder[Request, Params, Result, Response]) WithMaxBytesfunc (Builder[Request, Params, Result, Response]) WithStatusfunc (Builder[Request, Params, Result, Response]) WithTextfunc (Builder[Request, Params, Result, Response]) WithTextFirst
type WireRequest
Constants
const DefaultMaxBytes = 1 << 20DefaultMaxBytes is the largest request body a handler reads by default.
Variables
var (
// ErrUnsupportedMediaType is the 415 problem's error for a body that
// isn't JSON.
ErrUnsupportedMediaType = errors.New("request body must be JSON")
// ErrNotAcceptable is the 406 problem's error for an Accept field that
// accepts none of the handler's media types.
ErrNotAcceptable = errors.New("no acceptable media type")
// ErrTrailingData is the 400 problem's error for a body with more than
// one JSON value.
ErrTrailingData = errors.New("unexpected data after the JSON value")
)Types
Builder source
type Builder[Request WireRequest[Params], Params, Result, Response any] struct {
// contains filtered or unexported fields
}Builder configures a JSON handler. Its With methods return a modified copy.
New source
func New[Request WireRequest[Params], Params, Result, Response any](
op func(context.Context, Params) (Result, error),
response func(Result) Response,
) Builder[Request, Params, Result, Response]New returns a Builder for a handler that calls op, and converts its result
with response. It panics if either is nil. ExampleNew shows an endpoint wired from an operation, with a declared
error mapped to a 409 problem. Output:Example
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"funnyhaha.technology/httpkit/jsonhandler"
"funnyhaha.technology/httpkit/problem"
"funnyhaha.technology/rfc/rfc9110"
)
var (
errMissingName = errors.New("missing name")
errNameTaken = errors.New("name taken")
errDatabase = errors.New("connection refused")
)
type greetParams struct{ Name string }
type greetResult struct{ Greeting string }
type greetRequest struct {
Name string `json:"name"`
}
type greetResponse struct {
Greeting string `json:"greeting"`
}
func (req greetRequest) Params(r *http.Request) (greetParams, error) {
name := req.Name
if name == "" {
name = r.URL.Query().Get("name")
}
if name == "" {
return greetParams{}, errMissingName
}
return greetParams{Name: name}, nil
}
func newGreetResponse(res greetResult) greetResponse {
return greetResponse(res)
}
func greet(_ context.Context, p greetParams) (greetResult, error) {
switch p.Name {
case "taken":
return greetResult{}, fmt.Errorf("greet %s: %w", p.Name, errNameTaken)
case "db":
return greetResult{}, fmt.Errorf("greet: %w", errDatabase)
}
return greetResult{Greeting: "hello, " + p.Name}, nil
}
func main() {
h := jsonhandler.New[greetRequest](greet, newGreetResponse).
WithErrors(problem.ErrorStatus{Err: errNameTaken, Status: rfc9110.Conflict()}).
Handler()
rec := httptest.NewRecorder()
h(rec, httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/?name=taken", http.NoBody))
var p map[string]any
_ = json.Unmarshal(rec.Body.Bytes(), &p)
fmt.Println(rec.Code, p["detail"])
}
409 greet taken: name taken
(Builder[Request, Params, Result, Response]) Handler source
func (b Builder[Request, Params, Result, Response]) Handler() http.HandlerFuncHandler returns the handler.
(Builder[Request, Params, Result, Response]) WithCacheControl source
func (b Builder[Request, Params, Result, Response]) WithCacheControl(dirs ...rfc9111.Directive) Builder[Request, Params, Result, Response]WithCacheControl sets the Cache-Control field of a successful response.
(Builder[Request, Params, Result, Response]) WithErrors source
func (b Builder[Request, Params, Result, Response]) WithErrors(errs ...problem.ErrorStatus) Builder[Request, Params, Result, Response]WithErrors maps the operation's declared errors, and errors from the wire request's Params method, to problems. They're matched in order.
(Builder[Request, Params, Result, Response]) WithMaxBytes source
func (b Builder[Request, Params, Result, Response]) WithMaxBytes(n int64) Builder[Request, Params, Result, Response]WithMaxBytes sets the largest request body the handler reads, in bytes, as sent and as decoded. A larger body gets a 413.
(Builder[Request, Params, Result, Response]) WithStatus source
func (b Builder[Request, Params, Result, Response]) WithStatus(code int) Builder[Request, Params, Result, Response]WithStatus sets the status of a successful response, such as 201. It panics if code isn't a 2xx code.
(Builder[Request, Params, Result, Response]) WithText source
func (b Builder[Request, Params, Result, Response]) WithText(format func(Response) string) Builder[Request, Params, Result, Response]WithText adds a text/plain representation of the response, for clients that prefer it. JSON stays the default, for a client that accepts both equally, such as one that sends "Accept: */*".
(Builder[Request, Params, Result, Response]) WithTextFirst source
func (b Builder[Request, Params, Result, Response]) WithTextFirst(format func(Response) string) Builder[Request, Params, Result, Response]WithTextFirst is like WithText, but makes text/plain the default, so a client such as curl, which sends "Accept: */*", gets text.
WireRequest source
type WireRequest[Params any] interface {
Params(r *http.Request) (Params, error)
}WireRequest is a decoded wire request that converts itself to an operation's params. It receives the request, so it can read path and query values as well as the decoded body. It checks the input's shape and parses each value, returning a declared error for anything invalid.