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.
89 lines
2.0 KiB
89 lines
2.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
|
|
}
|
|
|
|
switch any(resp).(type) {
|
|
case Empty200:
|
|
w.WriteHeader(http.StatusOK)
|
|
default:
|
|
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)
|
|
}
|
|
|
|
// Empty200 is a sentinel response type. If a handler results in this type, an empty 200 OK is written.
|
|
type Empty200 struct{}
|
|
|