// Package handlers implements HTTP request handlers for the // netwatch-server API. package handlers import ( "context" "encoding/json" "errors" "log/slog" "mime" "net/http" "sneak.berlin/go/netwatch/internal/globals" "sneak.berlin/go/netwatch/internal/healthcheck" "sneak.berlin/go/netwatch/internal/logger" "sneak.berlin/go/netwatch/internal/reportbuf" "go.uber.org/fx" ) const jsonContentType = "application/json; charset=utf-8" // errNotJSON is what decodeJSON returns for a request whose // Content-Type is not application/json. var errNotJSON = errors.New("content type is not application/json") // reportAppender is the subset of the report buffer the handlers // depend on. Defining it here keeps the storage failure path // exercisable with a stub in tests. type reportAppender interface { Append(v any) error } // Params defines the dependencies for Handlers. type Params struct { fx.In Buffer *reportbuf.Buffer Globals *globals.Globals Healthcheck *healthcheck.Healthcheck Logger *logger.Logger } // Handlers provides HTTP handler factories for all endpoints. type Handlers struct { buf reportAppender hc *healthcheck.Healthcheck log *slog.Logger params *Params } // New creates a Handlers instance. func New( lc fx.Lifecycle, params Params, ) (*Handlers, error) { s := new(Handlers) s.buf = params.Buffer s.params = ¶ms s.log = params.Logger.Get() s.hc = params.Healthcheck lc.Append(fx.Hook{ OnStart: func(_ context.Context) error { return nil }, }) return s, nil } func (s *Handlers) respondJSON( w http.ResponseWriter, _ *http.Request, data any, status int, ) { w.Header().Set("Content-Type", jsonContentType) w.WriteHeader(status) if data != nil { err := json.NewEncoder(w).Encode(data) if err != nil { s.log.Error("json encode error", "error", err) } } } // decodeJSON decodes the request body into v. It returns errNotJSON, // reading nothing, unless the request's Content-Type is // application/json, with or without parameters such as charset. The // body is expected to already be bounded by the body-size middleware, // so a caller can distinguish an over-limit body from malformed JSON // by testing the returned error for *http.MaxBytesError. func (s *Handlers) decodeJSON( _ http.ResponseWriter, r *http.Request, v any, ) error { // A page on another origin can POST text/plain or a form without // a CORS preflight, so its visitors' browsers would send it // whatever the CORS settings. A JSON POST waits for the // preflight, so only JSON is taken. mediaType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) if err != nil || mediaType != "application/json" { return errNotJSON } return json.NewDecoder(r.Body).Decode(v) }