package httputil import ( "bytes" "context" "encoding/json" "errors" "io" "log/slog" "net/http" ) // MaxRequestBody caps a request body at 1 MiB. Endpoints that accept larger // payloads (media upload) override it explicitly. const MaxRequestBody = 1 << 20 type loggerKey struct{} // WithLogger attaches a request-scoped logger to the context. func WithLogger(ctx context.Context, l *slog.Logger) context.Context { return context.WithValue(ctx, loggerKey{}, l) } // LoggerFrom returns the request-scoped logger, or the default one. func LoggerFrom(ctx context.Context) *slog.Logger { if l, ok := ctx.Value(loggerKey{}).(*slog.Logger); ok && l != nil { return l } return slog.Default() } // WriteJSON writes v as JSON with the given status. Matrix requires // application/json on every response, including errors. func WriteJSON(w http.ResponseWriter, status int, v any) { body, err := json.Marshal(v) if err != nil { slog.Error("marshalling response failed", "error", err) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(`{"errcode":"M_UNKNOWN","error":"Internal server error"}`)) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if _, err := w.Write(body); err != nil { slog.Debug("writing response failed", "error", err) } } // WriteError sends err as a Matrix error envelope. Anything that is not a // *MatrixError is logged in full and reported as M_UNKNOWN, so internal // details never leak to the client. func WriteError(w http.ResponseWriter, r *http.Request, err error) { var merr *MatrixError if !errors.As(err, &merr) { LoggerFrom(r.Context()).Error("unhandled handler error", "error", err, "method", r.Method, "path", r.URL.Path) merr = InternalError() } if merr.status >= 500 { LoggerFrom(r.Context()).Error("request failed", "errcode", merr.Code, "error", merr.Message, "method", r.Method, "path", r.URL.Path) } WriteJSON(w, merr.StatusCode(), merr) } // DecodeJSON reads a JSON request body into v, enforcing MaxRequestBody and // mapping a missing, oversized or malformed body onto the right Matrix error. func DecodeJSON(w http.ResponseWriter, r *http.Request, v any) error { present, err := DecodeOptionalJSON(w, r, v) if err != nil { return err } if !present { return NotJSON() } return nil } // DecodeOptionalJSON is DecodeJSON for an endpoint whose body may legitimately // be absent, reporting whether there was one. A malformed body is still an // error: only a genuinely empty one is "not sent". // // DELETE /devices/{deviceId} is the case this exists for. A client sends it // with no body at all, because it does not know until the 401 comes back that // the endpoint is interactive. func DecodeOptionalJSON(w http.ResponseWriter, r *http.Request, v any) (bool, error) { body := http.MaxBytesReader(w, r.Body, MaxRequestBody) data, err := io.ReadAll(body) if err != nil { var tooLarge *http.MaxBytesError if errors.As(err, &tooLarge) { return false, NewError(http.StatusRequestEntityTooLarge, ErrCodeTooLarge, "Request body too large") } return false, NotJSON() } if len(bytes.TrimSpace(data)) == 0 { return false, nil } if err := json.Unmarshal(data, v); err != nil { var typeErr *json.UnmarshalTypeError if errors.As(err, &typeErr) { return false, BadJSON("Invalid value for field %q", typeErr.Field) } return false, NotJSON() } return true, nil } // Handler is an http.Handler whose errors are turned into Matrix envelopes by // Wrap, so handlers can `return httputil.Forbidden(...)` instead of writing // the response themselves. type Handler func(http.ResponseWriter, *http.Request) error // Wrap adapts a Handler to http.Handler. func Wrap(h Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if err := h(w, r); err != nil { WriteError(w, r, err) } }) }