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{}