diff --git a/internal/api/handlers/channels.go b/internal/api/handlers/channels.go index 9bf80d5..a43489a 100644 --- a/internal/api/handlers/channels.go +++ b/internal/api/handlers/channels.go @@ -5,6 +5,7 @@ package handlers import ( "encoding/hex" + "errors" "net/http" "strconv" "strings" @@ -12,6 +13,7 @@ import ( "github.com/MeshCore-Beacon/beacon-server/internal/api" "github.com/go-chi/chi/v5" + "github.com/jackc/pgx/v5" ) // ChannelsRouter mounts all /channels routes onto a subrouter. @@ -117,6 +119,7 @@ func listChannels(reader api.Reader) http.HandlerFunc { // @Success 200 {object} api.Channel // @Failure 400 {object} handlers.APIError // @Failure 404 {object} handlers.APIError +// @Failure 500 {object} handlers.APIError // @Router /channels/{channelID} [get] func getChannel(reader api.Reader) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -130,9 +133,13 @@ func getChannel(reader api.Reader) http.HandlerFunc { id = i } channel, err := reader.GetChannel(r.Context(), int32(id)) - if err != nil { + switch { + case errors.Is(err, pgx.ErrNoRows), err == nil && channel == nil: respondError(w, http.StatusNotFound, "channel not found") return + case err != nil: + respondError(w, http.StatusInternalServerError, "internal server error") + return } respond(w, http.StatusOK, channel) } diff --git a/internal/api/handlers/iatas.go b/internal/api/handlers/iatas.go index 1294c02..19d2375 100644 --- a/internal/api/handlers/iatas.go +++ b/internal/api/handlers/iatas.go @@ -4,11 +4,13 @@ package handlers import ( + "errors" "net/http" "strings" "github.com/MeshCore-Beacon/beacon-server/internal/api" "github.com/go-chi/chi/v5" + "github.com/jackc/pgx/v5" ) // IATAsRouter mounts all /iatas routes onto a subrouter. @@ -45,15 +47,18 @@ func pathIATA(r *http.Request) (string, bool) { // @Tags IATAs // @Produce json // @Success 200 {array} api.IATA -// @Failure 404 {object} handlers.APIError +// @Failure 500 {object} handlers.APIError // @Router /iatas [get] func listIATAs(reader api.Reader) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { iatas, err := reader.ListIATAs(r.Context()) if err != nil { - respondError(w, http.StatusNotFound, "no IATAs found") + respondError(w, http.StatusInternalServerError, "internal server error") return } + if iatas == nil { + iatas = []api.IATA{} + } respond(w, http.StatusOK, iatas) } } @@ -66,6 +71,7 @@ func listIATAs(reader api.Reader) http.HandlerFunc { // @Param iata path string true "3-letter IATA code" // @Success 200 {object} api.IATA // @Failure 404 {object} handlers.APIError +// @Failure 500 {object} handlers.APIError // @Router /iatas/{iata} [get] func getIATA(reader api.Reader) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -75,9 +81,13 @@ func getIATA(reader api.Reader) http.HandlerFunc { return } result, err := reader.GetIATA(r.Context(), iata) - if err != nil { + switch { + case errors.Is(err, pgx.ErrNoRows), err == nil && result == nil: respondError(w, http.StatusNotFound, "IATA not found") return + case err != nil: + respondError(w, http.StatusInternalServerError, "internal server error") + return } respond(w, http.StatusOK, result) } @@ -92,6 +102,7 @@ func getIATA(reader api.Reader) http.HandlerFunc { // @Success 200 {object} object "GeoJSON Feature (Polygon or MultiPolygon geometry, with bbox)" // @Success 204 "IATA exists but has no border configured" // @Failure 404 {object} handlers.APIError +// @Failure 500 {object} handlers.APIError // @Router /iatas/{iata}/border [get] func getIATABorder(reader api.Reader) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -101,9 +112,13 @@ func getIATABorder(reader api.Reader) http.HandlerFunc { return } border, err := reader.GetIATABorder(r.Context(), iata) - if err != nil { + switch { + case errors.Is(err, pgx.ErrNoRows): respondError(w, http.StatusNotFound, "IATA not found") return + case err != nil: + respondError(w, http.StatusInternalServerError, "internal server error") + return } // A cache round-trip re-encodes a nil json.RawMessage as the literal 4-byte JSON // "null" rather than leaving it empty, so both must be treated as "no border set". diff --git a/internal/api/handlers/iatas_test.go b/internal/api/handlers/iatas_test.go index b671771..f6e26df 100644 --- a/internal/api/handlers/iatas_test.go +++ b/internal/api/handlers/iatas_test.go @@ -6,13 +6,13 @@ package handlers import ( "context" "encoding/json" - "errors" "net/http" "net/http/httptest" "testing" "github.com/MeshCore-Beacon/beacon-server/internal/api" "github.com/go-chi/chi/v5" + "github.com/jackc/pgx/v5" ) func TestListIATAs_OK(t *testing.T) { @@ -148,7 +148,7 @@ func TestGetIATABorder_NotFound(t *testing.T) { r := chi.NewRouter() r.Get("/iatas/{iata}/border", getIATABorder(stubReader{ getIATABorder: func(_ context.Context, _ string) (json.RawMessage, error) { - return nil, errors.New("not found") + return nil, pgx.ErrNoRows }, })) req := httptest.NewRequest(http.MethodGet, "/iatas/ZZZ/border", nil) diff --git a/internal/api/handlers/lookup_errors_test.go b/internal/api/handlers/lookup_errors_test.go new file mode 100644 index 0000000..4c2431d --- /dev/null +++ b/internal/api/handlers/lookup_errors_test.go @@ -0,0 +1,116 @@ +// Copyright 2026 Beacon Contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +package handlers + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/MeshCore-Beacon/beacon-server/internal/api" + "github.com/google/uuid" + "github.com/jackc/pgx/v5" +) + +// Lookups answer 404 only for a missing row; any other failure is a retryable 500. +func TestLookupErrors(t *testing.T) { + id := uuid.NewString() + for _, tc := range []struct { + name string + router func(error) http.Handler + path string + missing int // status for pgx.ErrNoRows + }{ + {"region", func(err error) http.Handler { + return RegionsRouter(stubReader{getRegion: func(context.Context, int32) (*api.Region, error) { return nil, err }}) + }, "/1", http.StatusNotFound}, + {"regions", func(err error) http.Handler { + return RegionsRouter(stubReader{listRegions: func(context.Context) ([]api.RegionSummary, error) { return nil, err }}) + }, "/", http.StatusInternalServerError}, + {"iata", func(err error) http.Handler { + return IATAsRouter(stubReader{getIATA: func(context.Context, string) (*api.IATA, error) { return nil, err }}) + }, "/YOW", http.StatusNotFound}, + {"iata border", func(err error) http.Handler { + return IATAsRouter(stubReader{getIATABorder: func(context.Context, string) (json.RawMessage, error) { return nil, err }}) + }, "/YOW/border", http.StatusNotFound}, + {"iatas", func(err error) http.Handler { + return IATAsRouter(stubReader{listIATAs: func(context.Context) ([]api.IATA, error) { return nil, err }}) + }, "/", http.StatusInternalServerError}, + {"channel", func(err error) http.Handler { + return ChannelsRouter(stubReader{getChannel: func(context.Context, int32) (*api.Channel, error) { return nil, err }}) + }, "/7", http.StatusNotFound}, + {"observer", func(err error) http.Handler { + return ObserversRouter(stubReader{getObserver: func(context.Context, uuid.UUID) (*api.Observer, error) { return nil, err }}) + }, "/" + id, http.StatusNotFound}, + {"node", func(err error) http.Handler { + return NodesRouter(stubReader{getNode: func(context.Context, uuid.UUID) (*api.Node, error) { return nil, err }}) + }, "/" + id, http.StatusNotFound}, + {"packet", func(err error) http.Handler { + return PacketsRouter(stubReader{getPacket: func(context.Context, []byte) (*api.Packet, error) { return nil, err }}) + }, "/abcd", http.StatusNotFound}, + } { + for _, c := range []struct { + err error + want int + }{ + {pgx.ErrNoRows, tc.missing}, + {context.DeadlineExceeded, http.StatusInternalServerError}, + } { + t.Run(tc.name+"/"+c.err.Error(), func(t *testing.T) { + w := httptest.NewRecorder() + tc.router(c.err).ServeHTTP(w, httptest.NewRequest(http.MethodGet, tc.path, nil)) + if w.Code != c.want { + t.Fatalf("status = %d, want %d", w.Code, c.want) + } + var body map[string]APIError + if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil || body["error"].Message == "" { + t.Fatalf("not a JSON error: %s", w.Body.String()) + } + }) + } + } +} + +// An unknown region filter is the caller's mistake; a failed lookup is retryable. +func TestRegionFilterErrors(t *testing.T) { + for _, tc := range []struct { + err error + want int + }{ + {pgx.ErrNoRows, http.StatusBadRequest}, + {context.DeadlineExceeded, http.StatusInternalServerError}, + } { + reader := stubReader{getRegionBySlug: func(context.Context, string) (*api.Region, error) { return nil, tc.err }} + for name, h := range map[string]http.Handler{ + "packets": PacketsRouter(reader), + "nodes": NodesRouter(reader), + "traces": TracesRouter(reader), + } { + t.Run(name+"/"+tc.err.Error(), func(t *testing.T) { + w := httptest.NewRecorder() + h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/?region=nowhere", nil)) + if w.Code != tc.want { + t.Fatalf("status = %d, want %d", w.Code, tc.want) + } + }) + } + } +} + +func TestEmptyListsAreArrays(t *testing.T) { + for name, h := range map[string]http.Handler{ + "regions": RegionsRouter(stubReader{}), + "iatas": IATAsRouter(stubReader{}), + } { + t.Run(name, func(t *testing.T) { + w := httptest.NewRecorder() + h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/", nil)) + if w.Code != http.StatusOK || w.Body.String() != "[]\n" { + t.Fatalf("got %d %q, want 200 []", w.Code, w.Body.String()) + } + }) + } +} diff --git a/internal/api/handlers/messages.go b/internal/api/handlers/messages.go index 7d10fb8..2bd627b 100644 --- a/internal/api/handlers/messages.go +++ b/internal/api/handlers/messages.go @@ -147,7 +147,7 @@ func listMessagesBackfill(reader api.Reader) http.HandlerFunc { if regionIDStr := r.URL.Query().Get("regionId"); regionIDStr != "" || r.URL.Query().Get("region") != "" { regionIATAs, err := resolveRegionIATAs(r.Context(), r.URL.Query().Get("regionId"), r.URL.Query().Get("region"), reader) if err != nil { - respondError(w, http.StatusBadRequest, err.Error()) + respondRegionError(w, err) return } iatas = append(iatas, regionIATAs...) diff --git a/internal/api/handlers/nodes.go b/internal/api/handlers/nodes.go index d844a30..1d2f09a 100644 --- a/internal/api/handlers/nodes.go +++ b/internal/api/handlers/nodes.go @@ -5,6 +5,7 @@ package handlers import ( "encoding/hex" + "errors" "net/http" "strconv" "strings" @@ -12,6 +13,7 @@ import ( "github.com/MeshCore-Beacon/beacon-server/internal/api" "github.com/go-chi/chi/v5" "github.com/google/uuid" + "github.com/jackc/pgx/v5" ) // NodesRouter mounts all /nodes routes onto a subrouter. @@ -99,7 +101,7 @@ func listNodes(reader api.Reader) http.HandlerFunc { if regionIDStr := r.URL.Query().Get("regionId"); regionIDStr != "" || r.URL.Query().Get("region") != "" { regionIATAs, err := resolveRegionIATAs(r.Context(), regionIDStr, r.URL.Query().Get("region"), reader) if err != nil { - respondError(w, http.StatusBadRequest, err.Error()) + respondRegionError(w, err) return } iatas = append(iatas, regionIATAs...) @@ -156,6 +158,7 @@ func listNodes(reader api.Reader) http.HandlerFunc { // @Success 200 {object} api.Node // @Failure 400 {object} handlers.APIError // @Failure 404 {object} handlers.APIError +// @Failure 500 {object} handlers.APIError // @Router /nodes/{nodeId} [get] func getNode(reader api.Reader) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -165,9 +168,13 @@ func getNode(reader api.Reader) http.HandlerFunc { return } node, err := reader.GetNode(r.Context(), nodeID) - if err != nil { + switch { + case errors.Is(err, pgx.ErrNoRows), err == nil && node == nil: respondError(w, http.StatusNotFound, "node not found") return + case err != nil: + respondError(w, http.StatusInternalServerError, "internal server error") + return } respond(w, http.StatusOK, node) } diff --git a/internal/api/handlers/observer_comparison.go b/internal/api/handlers/observer_comparison.go index 6988d50..ff03db4 100644 --- a/internal/api/handlers/observer_comparison.go +++ b/internal/api/handlers/observer_comparison.go @@ -64,7 +64,7 @@ func getObserverComparison(reader api.Reader) http.HandlerFunc { if q.Get("regionId") != "" || q.Get("region") != "" { regionIATAs, err := resolveRegionIATAs(ctx, q.Get("regionId"), q.Get("region"), reader) if err != nil { - respondError(w, http.StatusBadRequest, "region not found") + respondRegionError(w, err) return } iatas = append(iatas, regionIATAs...) diff --git a/internal/api/handlers/observers.go b/internal/api/handlers/observers.go index 89fddd6..089c6e4 100644 --- a/internal/api/handlers/observers.go +++ b/internal/api/handlers/observers.go @@ -79,7 +79,7 @@ func listObservers(reader api.Reader) http.HandlerFunc { if regionIDStr := r.URL.Query().Get("regionId"); regionIDStr != "" || r.URL.Query().Get("region") != "" { regionIATAs, err := resolveRegionIATAs(r.Context(), regionIDStr, r.URL.Query().Get("region"), reader) if err != nil { - respondError(w, http.StatusBadRequest, err.Error()) + respondRegionError(w, err) return } iatas = append(iatas, regionIATAs...) @@ -102,6 +102,7 @@ func listObservers(reader api.Reader) http.HandlerFunc { // @Success 200 {object} api.Observer // @Failure 400 {object} handlers.APIError // @Failure 404 {object} handlers.APIError +// @Failure 500 {object} handlers.APIError // @Router /observers/{observerId} [get] func getObserver(reader api.Reader) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -112,9 +113,13 @@ func getObserver(reader api.Reader) http.HandlerFunc { return } obs, err := reader.GetObserver(r.Context(), id) - if err != nil { + switch { + case errors.Is(err, pgx.ErrNoRows), err == nil && obs == nil: respondError(w, http.StatusNotFound, "observer not found") return + case err != nil: + respondError(w, http.StatusInternalServerError, "internal server error") + return } respond(w, http.StatusOK, obs) } diff --git a/internal/api/handlers/packets.go b/internal/api/handlers/packets.go index 40ea63c..82f1026 100644 --- a/internal/api/handlers/packets.go +++ b/internal/api/handlers/packets.go @@ -5,6 +5,7 @@ package handlers import ( "encoding/hex" + "errors" "net/http" "strconv" "strings" @@ -12,6 +13,7 @@ import ( "github.com/MeshCore-Beacon/beacon-server/internal/api" "github.com/go-chi/chi/v5" + "github.com/jackc/pgx/v5" ) // PacketsRouter mounts all /packets routes onto a subrouter. @@ -102,7 +104,7 @@ func listPackets(reader api.Reader) http.HandlerFunc { if regionIDStr := r.URL.Query().Get("regionId"); regionIDStr != "" || r.URL.Query().Get("region") != "" { regionIATAs, err := resolveRegionIATAs(r.Context(), regionIDStr, r.URL.Query().Get("region"), reader) if err != nil { - respondError(w, http.StatusBadRequest, err.Error()) + respondRegionError(w, err) return } iatas = append(iatas, regionIATAs...) @@ -172,7 +174,7 @@ func listPacketsBackfill(reader api.Reader) http.HandlerFunc { if regionIDStr := r.URL.Query().Get("regionId"); regionIDStr != "" || r.URL.Query().Get("region") != "" { regionIATAs, err := resolveRegionIATAs(r.Context(), r.URL.Query().Get("regionId"), r.URL.Query().Get("region"), reader) if err != nil { - respondError(w, http.StatusBadRequest, err.Error()) + respondRegionError(w, err) return } iatas = append(iatas, regionIATAs...) @@ -207,9 +209,13 @@ func getPacket(reader api.Reader) http.HandlerFunc { return } packet, err := reader.GetPacket(r.Context(), hash) - if err != nil { + switch { + case errors.Is(err, pgx.ErrNoRows), err == nil && packet == nil: respondError(w, http.StatusNotFound, "packet not found") return + case err != nil: + respondError(w, http.StatusInternalServerError, "internal server error") + return } respond(w, http.StatusOK, packet) } diff --git a/internal/api/handlers/paths.go b/internal/api/handlers/paths.go index 7dfcc2b..7f25169 100644 --- a/internal/api/handlers/paths.go +++ b/internal/api/handlers/paths.go @@ -43,7 +43,7 @@ func getPathStats(reader api.Reader) http.HandlerFunc { if q.Get("regionId") != "" || q.Get("region") != "" { regionIATAs, err := resolveRegionIATAs(ctx, q.Get("regionId"), q.Get("region"), reader) if err != nil { - respondError(w, http.StatusBadRequest, "region not found") + respondRegionError(w, err) return } iatas = append(iatas, regionIATAs...) diff --git a/internal/api/handlers/paths_test.go b/internal/api/handlers/paths_test.go index fb3b0ee..b7be420 100644 --- a/internal/api/handlers/paths_test.go +++ b/internal/api/handlers/paths_test.go @@ -14,6 +14,7 @@ import ( "time" "github.com/MeshCore-Beacon/beacon-server/internal/api" + "github.com/jackc/pgx/v5" ) type pathReader struct { @@ -53,7 +54,7 @@ func TestPathsFiltersErrorsAndCancellation(t *testing.T) { return &api.Region{}, nil } if slug == "missing" { - return nil, errors.New("not found") + return nil, pgx.ErrNoRows } return &api.Region{IATAs: []string{"YVR"}}, nil }}, read: func(ctx context.Context, since, until time.Time, iatas []string) (*api.PathStats, error) { diff --git a/internal/api/handlers/regions.go b/internal/api/handlers/regions.go index f8366e8..8437f41 100644 --- a/internal/api/handlers/regions.go +++ b/internal/api/handlers/regions.go @@ -5,6 +5,7 @@ package handlers import ( "context" + "errors" "fmt" "net/http" "strconv" @@ -13,6 +14,7 @@ import ( "github.com/MeshCore-Beacon/beacon-server/internal/api" "github.com/go-chi/chi/v5" + "github.com/jackc/pgx/v5" ) // RegionsRouter mounts all /regions routes onto a subrouter. @@ -35,15 +37,18 @@ func RegionsRouter(reader api.Reader) http.Handler { // @Tags Regions // @Produce json // @Success 200 {array} api.RegionSummary -// @Failure 404 {object} handlers.APIError +// @Failure 500 {object} handlers.APIError // @Router /regions [get] func listRegions(reader api.Reader) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { regions, err := reader.ListRegions(r.Context()) if err != nil { - respondError(w, http.StatusNotFound, "no regions found") + respondError(w, http.StatusInternalServerError, "internal server error") return } + if regions == nil { + regions = []api.RegionSummary{} + } respond(w, http.StatusOK, regions) } } @@ -57,6 +62,7 @@ func listRegions(reader api.Reader) http.HandlerFunc { // @Success 200 {object} api.Region // @Failure 400 {object} handlers.APIError // @Failure 404 {object} handlers.APIError +// @Failure 500 {object} handlers.APIError // @Router /regions/{regionId} [get] func getRegion(reader api.Reader) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -67,9 +73,13 @@ func getRegion(reader api.Reader) http.HandlerFunc { return } region, err := reader.GetRegion(r.Context(), int32(regionInt)) - if err != nil { + switch { + case errors.Is(err, pgx.ErrNoRows), err == nil && region == nil: respondError(w, http.StatusNotFound, "region not found") return + case err != nil: + respondError(w, http.StatusInternalServerError, "internal server error") + return } respond(w, http.StatusOK, region) } @@ -191,3 +201,13 @@ func resolveRegionIATAs(ctx context.Context, regionID, regionSlug string, reader } return region.IATAs, nil } + +// respondRegionError reports an unknown or malformed region filter as 400 and a +// failed lookup as a retryable 500. +func respondRegionError(w http.ResponseWriter, err error) { + if errors.Is(err, pgx.ErrNoRows) || errors.Is(err, strconv.ErrSyntax) || errors.Is(err, strconv.ErrRange) { + respondError(w, http.StatusBadRequest, err.Error()) + return + } + respondError(w, http.StatusInternalServerError, "internal server error") +} diff --git a/internal/api/handlers/scope_stats_test.go b/internal/api/handlers/scope_stats_test.go index 363b1c8..9d30e3f 100644 --- a/internal/api/handlers/scope_stats_test.go +++ b/internal/api/handlers/scope_stats_test.go @@ -15,6 +15,7 @@ import ( "github.com/MeshCore-Beacon/beacon-server/internal/api" "github.com/MeshCore-Beacon/beacon-server/internal/scopestore" + "github.com/jackc/pgx/v5" ) func TestGetStatsScopes_RegionErrors(t *testing.T) { @@ -22,10 +23,10 @@ func TestGetStatsScopes_RegionErrors(t *testing.T) { t.Run(query, func(t *testing.T) { reader := stubReader{ getRegion: func(context.Context, int32) (*api.Region, error) { - return nil, errors.New("not found") + return nil, pgx.ErrNoRows }, getRegionBySlug: func(context.Context, string) (*api.Region, error) { - return nil, errors.New("not found") + return nil, pgx.ErrNoRows }, } w := httptest.NewRecorder() diff --git a/internal/api/handlers/scopes.go b/internal/api/handlers/scopes.go index 9ca791e..d629323 100644 --- a/internal/api/handlers/scopes.go +++ b/internal/api/handlers/scopes.go @@ -55,7 +55,7 @@ func listScopes(reader api.Reader, scopes ScopeMembership) http.HandlerFunc { if regionID != "" || region != "" { regionIATAs, err := resolveRegionIATAs(r.Context(), regionID, region, reader) if err != nil { - respondError(w, http.StatusBadRequest, err.Error()) + respondRegionError(w, err) return } iatas = append(iatas, regionIATAs...) diff --git a/internal/api/handlers/signal.go b/internal/api/handlers/signal.go index 34995f0..1a2563d 100644 --- a/internal/api/handlers/signal.go +++ b/internal/api/handlers/signal.go @@ -43,7 +43,7 @@ func getSignalStats(reader api.Reader) http.HandlerFunc { if q.Get("regionId") != "" || q.Get("region") != "" { regionIATAs, err := resolveRegionIATAs(ctx, q.Get("regionId"), q.Get("region"), reader) if err != nil { - respondError(w, http.StatusBadRequest, "region not found") + respondRegionError(w, err) return } iatas = append(iatas, regionIATAs...) diff --git a/internal/api/handlers/signal_test.go b/internal/api/handlers/signal_test.go index 7d29d4b..5cc234b 100644 --- a/internal/api/handlers/signal_test.go +++ b/internal/api/handlers/signal_test.go @@ -14,6 +14,7 @@ import ( "time" "github.com/MeshCore-Beacon/beacon-server/internal/api" + "github.com/jackc/pgx/v5" ) type signalReader struct { @@ -56,7 +57,7 @@ func TestSignalFiltersAndErrors(t *testing.T) { case "empty": return &api.Region{}, nil case "missing": - return nil, errors.New("not found") + return nil, pgx.ErrNoRows default: return &api.Region{IATAs: []string{"YVR"}}, nil } diff --git a/internal/api/handlers/stats.go b/internal/api/handlers/stats.go index 43164d0..426ad83 100644 --- a/internal/api/handlers/stats.go +++ b/internal/api/handlers/stats.go @@ -86,7 +86,7 @@ func getStatsOverview(reader api.Reader) http.HandlerFunc { if regionIDStr := r.URL.Query().Get("regionId"); regionIDStr != "" || r.URL.Query().Get("region") != "" { regionIATAs, err := resolveRegionIATAs(r.Context(), regionIDStr, r.URL.Query().Get("region"), reader) if err != nil { - respondError(w, http.StatusBadRequest, err.Error()) + respondRegionError(w, err) return } iatas = append(iatas, regionIATAs...) @@ -123,7 +123,7 @@ func getStatsObservations(reader api.Reader) http.HandlerFunc { if regionIDStr := r.URL.Query().Get("regionId"); regionIDStr != "" || r.URL.Query().Get("region") != "" { regionIATAs, err := resolveRegionIATAs(r.Context(), regionIDStr, r.URL.Query().Get("region"), reader) if err != nil { - respondError(w, http.StatusBadRequest, err.Error()) + respondRegionError(w, err) return } iatas = append(iatas, regionIATAs...) @@ -164,7 +164,7 @@ func getStatsPayloadBreakdown(reader api.Reader) http.HandlerFunc { if regionIDStr := r.URL.Query().Get("regionId"); regionIDStr != "" || r.URL.Query().Get("region") != "" { regionIATAs, err := resolveRegionIATAs(r.Context(), regionIDStr, r.URL.Query().Get("region"), reader) if err != nil { - respondError(w, http.StatusBadRequest, err.Error()) + respondRegionError(w, err) return } iatas = append(iatas, regionIATAs...) @@ -207,7 +207,7 @@ func getStatsTopNodes(reader api.Reader) http.HandlerFunc { if regionIDStr := r.URL.Query().Get("regionId"); regionIDStr != "" || r.URL.Query().Get("region") != "" { regionIATAs, err := resolveRegionIATAs(r.Context(), regionIDStr, r.URL.Query().Get("region"), reader) if err != nil { - respondError(w, http.StatusBadRequest, err.Error()) + respondRegionError(w, err) return } iatas = append(iatas, regionIATAs...) @@ -254,7 +254,7 @@ func getStatsTopObservers(reader api.Reader) http.HandlerFunc { if regionIDStr := r.URL.Query().Get("regionId"); regionIDStr != "" || r.URL.Query().Get("region") != "" { regionIATAs, err := resolveRegionIATAs(r.Context(), regionIDStr, r.URL.Query().Get("region"), reader) if err != nil { - respondError(w, http.StatusBadRequest, err.Error()) + respondRegionError(w, err) return } iatas = append(iatas, regionIATAs...) @@ -302,7 +302,7 @@ func getStatsTopAdvertisers(reader api.Reader) http.HandlerFunc { if regionIDStr := r.URL.Query().Get("regionId"); regionIDStr != "" || r.URL.Query().Get("region") != "" { regionIATAs, err := resolveRegionIATAs(r.Context(), regionIDStr, r.URL.Query().Get("region"), reader) if err != nil { - respondError(w, http.StatusBadRequest, err.Error()) + respondRegionError(w, err) return } iatas = append(iatas, regionIATAs...) @@ -348,7 +348,7 @@ func getStatsClockDrift(reader api.Reader) http.HandlerFunc { if regionIDStr := r.URL.Query().Get("regionId"); regionIDStr != "" || r.URL.Query().Get("region") != "" { regionIATAs, err := resolveRegionIATAs(r.Context(), regionIDStr, r.URL.Query().Get("region"), reader) if err != nil { - respondError(w, http.StatusBadRequest, err.Error()) + respondRegionError(w, err) return } iatas = append(iatas, regionIATAs...) @@ -390,7 +390,7 @@ func getStatsTopTalkers(reader api.Reader) http.HandlerFunc { if regionIDStr := r.URL.Query().Get("regionId"); regionIDStr != "" || r.URL.Query().Get("region") != "" { regionIATAs, err := resolveRegionIATAs(r.Context(), regionIDStr, r.URL.Query().Get("region"), reader) if err != nil { - respondError(w, http.StatusBadRequest, err.Error()) + respondRegionError(w, err) return } iatas = append(iatas, regionIATAs...) @@ -437,7 +437,7 @@ func getStatsRadioPresets(reader api.Reader) http.HandlerFunc { if regionIDStr := r.URL.Query().Get("regionId"); regionIDStr != "" || r.URL.Query().Get("region") != "" { regionIATAs, err := resolveRegionIATAs(r.Context(), regionIDStr, r.URL.Query().Get("region"), reader) if err != nil { - respondError(w, http.StatusBadRequest, err.Error()) + respondRegionError(w, err) return } iatas = append(iatas, regionIATAs...) @@ -475,7 +475,7 @@ func getStatsScopes(reader api.Reader, scopes ScopeMembership) http.HandlerFunc if regionID := r.URL.Query().Get("regionId"); regionID != "" || r.URL.Query().Get("region") != "" { regionIATAs, err := resolveRegionIATAs(r.Context(), regionID, r.URL.Query().Get("region"), reader) if err != nil { - respondError(w, http.StatusBadRequest, err.Error()) + respondRegionError(w, err) return } iatas = append(iatas, regionIATAs...) @@ -522,7 +522,7 @@ func getStatsNodeTypes(reader api.Reader) http.HandlerFunc { if regionIDStr := r.URL.Query().Get("regionId"); regionIDStr != "" || r.URL.Query().Get("region") != "" { regionIATAs, err := resolveRegionIATAs(r.Context(), regionIDStr, r.URL.Query().Get("region"), reader) if err != nil { - respondError(w, http.StatusBadRequest, err.Error()) + respondRegionError(w, err) return } iatas = append(iatas, regionIATAs...) diff --git a/internal/api/handlers/stats_series.go b/internal/api/handlers/stats_series.go index 564a701..961d58a 100644 --- a/internal/api/handlers/stats_series.go +++ b/internal/api/handlers/stats_series.go @@ -54,7 +54,7 @@ func getStatsSeries(reader api.Reader, maxWindow time.Duration) http.HandlerFunc if q.Get("regionId") != "" || q.Get("region") != "" { regionIATAs, err := resolveRegionIATAs(ctx, q.Get("regionId"), q.Get("region"), reader) if err != nil { - respondError(w, http.StatusBadRequest, "region not found") + respondRegionError(w, err) return } iatas = append(iatas, regionIATAs...) diff --git a/internal/api/handlers/stats_series_test.go b/internal/api/handlers/stats_series_test.go index 3ba2624..68b34f8 100644 --- a/internal/api/handlers/stats_series_test.go +++ b/internal/api/handlers/stats_series_test.go @@ -14,6 +14,7 @@ import ( "time" "github.com/MeshCore-Beacon/beacon-server/internal/api" + "github.com/jackc/pgx/v5" ) func TestGetStatsSeries(t *testing.T) { @@ -49,7 +50,7 @@ func TestGetStatsSeries(t *testing.T) { case "empty": return &api.Region{}, nil case "missing": - return nil, errors.New("not found") + return nil, pgx.ErrNoRows } return &api.Region{IATAs: []string{"YVR"}}, nil }, diff --git a/internal/api/handlers/traces.go b/internal/api/handlers/traces.go index 9f131b5..9ce002d 100644 --- a/internal/api/handlers/traces.go +++ b/internal/api/handlers/traces.go @@ -83,7 +83,7 @@ func listTraceTags(reader api.Reader) http.HandlerFunc { if regionIDStr := r.URL.Query().Get("regionId"); regionIDStr != "" || r.URL.Query().Get("region") != "" { regionIATAs, err := resolveRegionIATAs(r.Context(), r.URL.Query().Get("regionId"), r.URL.Query().Get("region"), reader) if err != nil { - respondError(w, http.StatusBadRequest, err.Error()) + respondRegionError(w, err) return } iatas = append(iatas, regionIATAs...)