You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
scom/shttp/handlers.go

135 lines
3.0 KiB

package shttp
import (
"bufio"
"context"
"encoding/json"
"errors"
"io"
"log/slog"
"net/http"
)
type ctxKeyReq struct{}
func WithRequest(ctx context.Context, r *http.Request) context.Context {
return context.WithValue(ctx, ctxKeyReq{}, r)
}
func RequestFromContext(ctx context.Context) *http.Request {
r, _ := ctx.Value(ctxKeyReq{}).(*http.Request)
return r
}
// JSONHandler serves a JSON request with a JSON response.
type JSONHandler[TReq any, TResp any] struct {
BodyOptional bool
Handler func(context.Context, TReq) (TResp, error)
ErrHandler func(http.ResponseWriter, *http.Request, string, error)
}
func (j JSONHandler[TReq, TResp]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx = WithRequest(ctx, r)
body := new(TReq)
bodyBuf := bufio.NewReader(r.Body)
if _, err := bodyBuf.Peek(1); err != nil {
if !errors.Is(err, io.EOF) {
j.handleErr(w, r, "reading request", err, http.StatusBadRequest)
return
}
if !j.BodyOptional {
j.handleErr(w, r, "missing request body", err, http.StatusBadRequest)
return
}
}
err := json.NewDecoder(bodyBuf).Decode(body)
if err != nil {
j.handleErr(w, r, "decoding json request", err, http.StatusBadRequest)
return
}
resp, err := j.Handler(ctx, *body)
if err != nil {
j.handleErr(w, r, "handling request", err, http.StatusInternalServerError)
return
}
err = json.NewEncoder(w).Encode(resp)
if err != nil {
j.handleErr(w, r, "serving response", err, 0)
return
}
}
func (j JSONHandler[TReq, TResp]) handleErr(w http.ResponseWriter, r *http.Request, op string, err error, defaultStatus int) {
if j.ErrHandler != nil {
j.ErrHandler(w, r, op, err)
return
}
if defaultStatus > 0 {
w.WriteHeader(defaultStatus)
}
slog.WarnContext(r.Context(), op, "err", err)
}
// HTMLHandler serves a request with an HTML response.
type HTMLHandler[TReq any, TResp any] struct {
BodyOptional bool
Handler func(context.Context, TReq) (TResp, error)
ErrHandler func(http.ResponseWriter, *http.Request, string, error)
}
func (j HTMLHandler[TReq, TResp]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
body := new(TReq)
bodyBuf := bufio.NewReader(r.Body)
if _, err := bodyBuf.Peek(1); err != nil {
if !errors.Is(err, io.EOF) {
j.handleErr(w, r, "decoding json request", err)
return
}
if !j.BodyOptional {
j.handleErr(w, r, "missing request body", err)
return
}
}
err := json.NewDecoder(bodyBuf).Decode(body)
if err != nil {
j.handleErr(w, r, "decoding json request", err)
return
}
resp, err := j.Handler(ctx, *body)
if err != nil {
j.handleErr(w, r, "handling request", err)
return
}
err = json.NewEncoder(w).Encode(resp)
if err != nil {
j.handleErr(w, r, "serving response", err)
return
}
}
func (j HTMLHandler[TReq, TResp]) handleErr(w http.ResponseWriter, r *http.Request, op string, err error) {
if j.ErrHandler != nil {
j.ErrHandler(w, r, op, err)
return
}
slog.WarnContext(r.Context(), op, "err", err)
}