mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-10-22 21:11:26 +00:00
fix(api): answer 404 only for missing rows
Lookups turned every error into a 404, and region filters into a 400, so a transient database failure looked final to clients that don't retry 4xx. Missing rows stay 404/400; anything else is a 500. Empty /regions and /iatas lists are [] rather than null.
This commit is contained in:
1 parent
5b49356437
commit
a1aff810e9
20 files changed
+220
-40
No files matched your search
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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".
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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...)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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...)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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...)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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...)
|
||||
|
||||
@@ -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...)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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...)
|
||||
|
||||
@@ -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...)
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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...)
|
||||
|
||||
Reference in new issue
Block a user