fix: 503 при сбое GetScan вместо ложного 404 scan not found
- Store.GetScan возвращает (job, ok, error); Postgres отдаёт err при ошибке БД/JSON - API: getScanOrWriteError → 503 scan storage temporarily unavailable - Web UI: понятное сообщение для HTTP 503 при опросе скана Made-with: Cursor
This commit is contained in:
+37
-29
@@ -14,6 +14,9 @@ import (
|
|||||||
"nettopo-go/internal/webui"
|
"nettopo-go/internal/webui"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// errScanStorageUnavailable — ответ API при сбое GetScan (БД, а не «нет такого scan_id»).
|
||||||
|
const errScanStorageUnavailable = "scan storage temporarily unavailable"
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
store scans.Store
|
store scans.Store
|
||||||
run *scans.Runner
|
run *scans.Runner
|
||||||
@@ -83,6 +86,26 @@ func scanPathID(r *http.Request) string {
|
|||||||
return strings.TrimSpace(raw)
|
return strings.TrimSpace(raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getScanOrWriteError загружает скан; при ошибке хранилища пишет 503, при отсутствии строки — 404 с notFoundMsg (или «scan not found»).
|
||||||
|
func (h *Handler) getScanOrWriteError(w http.ResponseWriter, id, notFoundMsg string) (scans.ScanJob, bool) {
|
||||||
|
job, found, err := h.store.GetScan(id)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("GetScan id=%q (len=%d): %v", id, len(id), err)
|
||||||
|
writeError(w, http.StatusServiceUnavailable, errScanStorageUnavailable)
|
||||||
|
return scans.ScanJob{}, false
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
msg := notFoundMsg
|
||||||
|
if msg == "" {
|
||||||
|
msg = "scan not found"
|
||||||
|
}
|
||||||
|
log.Printf("getScan: not found id=%q (len=%d) msg=%q", id, len(id), msg)
|
||||||
|
writeError(w, http.StatusNotFound, msg)
|
||||||
|
return scans.ScanJob{}, false
|
||||||
|
}
|
||||||
|
return job, true
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) Routes() http.Handler {
|
func (h *Handler) Routes() http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
webFS := http.FileServer(http.FS(webui.FS))
|
webFS := http.FileServer(http.FS(webui.FS))
|
||||||
@@ -189,12 +212,10 @@ func (h *Handler) getScansDiff(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusBadRequest, "both from and to are required")
|
writeError(w, http.StatusBadRequest, "both from and to are required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, ok := h.store.GetScan(fromID); !ok {
|
if _, ok := h.getScanOrWriteError(w, fromID, "from scan not found"); !ok {
|
||||||
writeError(w, http.StatusNotFound, "from scan not found")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, ok := h.store.GetScan(toID); !ok {
|
if _, ok := h.getScanOrWriteError(w, toID, "to scan not found"); !ok {
|
||||||
writeError(w, http.StatusNotFound, "to scan not found")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,10 +242,8 @@ func (h *Handler) getScan(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
job, ok := h.store.GetScan(id)
|
job, ok := h.getScanOrWriteError(w, id, "")
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("getScan: not found id=%q (len=%d)", id, len(id))
|
|
||||||
writeError(w, http.StatusNotFound, "scan not found")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,9 +260,8 @@ func (h *Handler) postCancelScan(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusBadRequest, "scan id is required")
|
writeError(w, http.StatusBadRequest, "scan id is required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, ok := h.store.GetScan(id); !ok {
|
cur, ok := h.getScanOrWriteError(w, id, "")
|
||||||
log.Printf("postCancelScan: not found id=%q (len=%d)", id, len(id))
|
if !ok {
|
||||||
writeError(w, http.StatusNotFound, "scan not found")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
runnerSignaled := false
|
runnerSignaled := false
|
||||||
@@ -254,7 +272,6 @@ func (h *Handler) postCancelScan(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "runner_canceled": true})
|
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "runner_canceled": true})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cur, _ := h.store.GetScan(id)
|
|
||||||
if cur.Status == "queued" {
|
if cur.Status == "queued" {
|
||||||
fin := time.Now().UTC()
|
fin := time.Now().UTC()
|
||||||
_, _ = h.store.UpdateScan(id, scans.ScanUpdate{Status: "canceled", FinishedAt: &fin})
|
_, _ = h.store.UpdateScan(id, scans.ScanUpdate{Status: "canceled", FinishedAt: &fin})
|
||||||
@@ -270,8 +287,7 @@ func (h *Handler) getScanHosts(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusBadRequest, "scan id is required")
|
writeError(w, http.StatusBadRequest, "scan id is required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, ok := h.store.GetScan(id); !ok {
|
if _, ok := h.getScanOrWriteError(w, id, ""); !ok {
|
||||||
writeError(w, http.StatusNotFound, "scan not found")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,8 +312,7 @@ func (h *Handler) getScanPorts(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusBadRequest, "scan id is required")
|
writeError(w, http.StatusBadRequest, "scan id is required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, ok := h.store.GetScan(id); !ok {
|
if _, ok := h.getScanOrWriteError(w, id, ""); !ok {
|
||||||
writeError(w, http.StatusNotFound, "scan not found")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,8 +337,7 @@ func (h *Handler) getScanSNMP(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusBadRequest, "scan id is required")
|
writeError(w, http.StatusBadRequest, "scan id is required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, ok := h.store.GetScan(id); !ok {
|
if _, ok := h.getScanOrWriteError(w, id, ""); !ok {
|
||||||
writeError(w, http.StatusNotFound, "scan not found")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,8 +362,7 @@ func (h *Handler) getScanLLDP(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusBadRequest, "scan id is required")
|
writeError(w, http.StatusBadRequest, "scan id is required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, ok := h.store.GetScan(id); !ok {
|
if _, ok := h.getScanOrWriteError(w, id, ""); !ok {
|
||||||
writeError(w, http.StatusNotFound, "scan not found")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,8 +387,7 @@ func (h *Handler) getScanInterfaces(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusBadRequest, "scan id is required")
|
writeError(w, http.StatusBadRequest, "scan id is required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, ok := h.store.GetScan(id); !ok {
|
if _, ok := h.getScanOrWriteError(w, id, ""); !ok {
|
||||||
writeError(w, http.StatusNotFound, "scan not found")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ip := strings.TrimSpace(r.URL.Query().Get("ip"))
|
ip := strings.TrimSpace(r.URL.Query().Get("ip"))
|
||||||
@@ -403,8 +415,7 @@ func (h *Handler) getScanPortDevices(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusBadRequest, "scan id is required")
|
writeError(w, http.StatusBadRequest, "scan id is required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, ok := h.store.GetScan(id); !ok {
|
if _, ok := h.getScanOrWriteError(w, id, ""); !ok {
|
||||||
writeError(w, http.StatusNotFound, "scan not found")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ip := strings.TrimSpace(r.URL.Query().Get("ip"))
|
ip := strings.TrimSpace(r.URL.Query().Get("ip"))
|
||||||
@@ -463,8 +474,7 @@ func (h *Handler) getScanMacLocations(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusBadRequest, "scan id is required")
|
writeError(w, http.StatusBadRequest, "scan id is required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, ok := h.store.GetScan(id); !ok {
|
if _, ok := h.getScanOrWriteError(w, id, ""); !ok {
|
||||||
writeError(w, http.StatusNotFound, "scan not found")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
raw := strings.TrimSpace(r.URL.Query().Get("mac"))
|
raw := strings.TrimSpace(r.URL.Query().Get("mac"))
|
||||||
@@ -612,8 +622,7 @@ func (h *Handler) getScanLinks(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusBadRequest, "scan id is required")
|
writeError(w, http.StatusBadRequest, "scan id is required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, ok := h.store.GetScan(id); !ok {
|
if _, ok := h.getScanOrWriteError(w, id, ""); !ok {
|
||||||
writeError(w, http.StatusNotFound, "scan not found")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -675,8 +684,7 @@ func (h *Handler) getScanTopology(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusBadRequest, "scan id is required")
|
writeError(w, http.StatusBadRequest, "scan id is required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, ok := h.store.GetScan(id); !ok {
|
if _, ok := h.getScanOrWriteError(w, id, ""); !ok {
|
||||||
writeError(w, http.StatusNotFound, "scan not found")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -98,7 +99,7 @@ insert into scan_jobs (
|
|||||||
return job, nil
|
return job, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PostgresStore) GetScan(id string) (ScanJob, bool) {
|
func (s *PostgresStore) GetScan(id string) (ScanJob, bool, error) {
|
||||||
query := `
|
query := `
|
||||||
select id, name, status, cidrs::text, exclude_ips::text, options::text,
|
select id, name, status, cidrs::text, exclude_ips::text, options::text,
|
||||||
progress, stats::text, coalesce(snmp_credentials_id::text, ''),
|
progress, stats::text, coalesce(snmp_credentials_id::text, ''),
|
||||||
@@ -130,28 +131,28 @@ limit 1`
|
|||||||
&finishedAt,
|
&finishedAt,
|
||||||
)
|
)
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return ScanJob{}, false
|
return ScanJob{}, false, nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("postgres GetScan id=%q: row scan: %v", id, err)
|
log.Printf("postgres GetScan id=%q: row scan: %v", id, err)
|
||||||
return ScanJob{}, false
|
return ScanJob{}, false, fmt.Errorf("postgres GetScan id=%q: %w", id, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = json.Unmarshal([]byte(cidrsRaw), &job.CIDRs); err != nil {
|
if err = json.Unmarshal([]byte(cidrsRaw), &job.CIDRs); err != nil {
|
||||||
log.Printf("postgres GetScan id=%q: cidrs json (len=%d): %v", id, len(cidrsRaw), err)
|
log.Printf("postgres GetScan id=%q: cidrs json (len=%d): %v", id, len(cidrsRaw), err)
|
||||||
return ScanJob{}, false
|
return ScanJob{}, false, fmt.Errorf("postgres GetScan id=%q cidrs: %w", id, err)
|
||||||
}
|
}
|
||||||
if err = json.Unmarshal([]byte(excludeRaw), &job.ExcludeIPs); err != nil {
|
if err = json.Unmarshal([]byte(excludeRaw), &job.ExcludeIPs); err != nil {
|
||||||
log.Printf("postgres GetScan id=%q: exclude_ips json: %v", id, err)
|
log.Printf("postgres GetScan id=%q: exclude_ips json: %v", id, err)
|
||||||
return ScanJob{}, false
|
return ScanJob{}, false, fmt.Errorf("postgres GetScan id=%q exclude_ips: %w", id, err)
|
||||||
}
|
}
|
||||||
if err = json.Unmarshal([]byte(optionsRaw), &job.Options); err != nil {
|
if err = json.Unmarshal([]byte(optionsRaw), &job.Options); err != nil {
|
||||||
log.Printf("postgres GetScan id=%q: options json: %v", id, err)
|
log.Printf("postgres GetScan id=%q: options json: %v", id, err)
|
||||||
return ScanJob{}, false
|
return ScanJob{}, false, fmt.Errorf("postgres GetScan id=%q options: %w", id, err)
|
||||||
}
|
}
|
||||||
if err = json.Unmarshal([]byte(statsRaw), &job.Stats); err != nil {
|
if err = json.Unmarshal([]byte(statsRaw), &job.Stats); err != nil {
|
||||||
log.Printf("postgres GetScan id=%q: stats json: %v", id, err)
|
log.Printf("postgres GetScan id=%q: stats json: %v", id, err)
|
||||||
return ScanJob{}, false
|
return ScanJob{}, false, fmt.Errorf("postgres GetScan id=%q stats: %w", id, err)
|
||||||
}
|
}
|
||||||
if startedAt.Valid {
|
if startedAt.Valid {
|
||||||
job.StartedAt = startedAt.Time
|
job.StartedAt = startedAt.Time
|
||||||
@@ -160,7 +161,7 @@ limit 1`
|
|||||||
job.FinishedAt = finishedAt.Time
|
job.FinishedAt = finishedAt.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
return job, true
|
return job, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PostgresStore) ListScans(limit int) []ScanJob {
|
func (s *PostgresStore) ListScans(limit int) []ScanJob {
|
||||||
@@ -230,7 +231,11 @@ limit $1`
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *PostgresStore) UpdateScan(id string, update ScanUpdate) (ScanJob, bool) {
|
func (s *PostgresStore) UpdateScan(id string, update ScanUpdate) (ScanJob, bool) {
|
||||||
current, ok := s.GetScan(id)
|
current, ok, err := s.GetScan(id)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("postgres UpdateScan: GetScan id=%q: %v", id, err)
|
||||||
|
return ScanJob{}, false
|
||||||
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
return ScanJob{}, false
|
return ScanJob{}, false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,7 +88,11 @@ func (r *Runner) CancelScan(id string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Runner) markJobCanceledFromStore(jobID string) {
|
func (r *Runner) markJobCanceledFromStore(jobID string) {
|
||||||
cur, ok := r.store.GetScan(jobID)
|
cur, ok, err := r.store.GetScan(jobID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("runner markJobCanceledFromStore GetScan %q: %v", jobID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -106,7 +110,10 @@ func (r *Runner) markJobCanceledFromStore(jobID string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Runner) run(ctx context.Context, job ScanJob) {
|
func (r *Runner) run(ctx context.Context, job ScanJob) {
|
||||||
if cur, ok := r.store.GetScan(job.ID); ok && cur.Status == "canceled" {
|
if cur, ok, err := r.store.GetScan(job.ID); err != nil {
|
||||||
|
log.Printf("runner run GetScan %q: %v", job.ID, err)
|
||||||
|
return
|
||||||
|
} else if ok && cur.Status == "canceled" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := ctx.Err(); err != nil && errors.Is(err, context.Canceled) {
|
if err := ctx.Err(); err != nil && errors.Is(err, context.Canceled) {
|
||||||
|
|||||||
@@ -61,7 +61,8 @@ type ScanUpdate struct {
|
|||||||
|
|
||||||
type Store interface {
|
type Store interface {
|
||||||
CreateScan(CreateScanRequest) (ScanJob, error)
|
CreateScan(CreateScanRequest) (ScanJob, error)
|
||||||
GetScan(id string) (ScanJob, bool)
|
// GetScan: при found==false и err==nil записи нет; err!=nil — сбой хранилища (например БД).
|
||||||
|
GetScan(id string) (ScanJob, bool, error)
|
||||||
ListScans(limit int) []ScanJob
|
ListScans(limit int) []ScanJob
|
||||||
UpdateScan(id string, update ScanUpdate) (ScanJob, bool)
|
UpdateScan(id string, update ScanUpdate) (ScanJob, bool)
|
||||||
SaveHostResult(scanID string, host HostResult) error
|
SaveHostResult(scanID string, host HostResult) error
|
||||||
@@ -195,11 +196,11 @@ func (s *MemoryStore) CreateScan(req CreateScanRequest) (ScanJob, error) {
|
|||||||
return job, nil
|
return job, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MemoryStore) GetScan(id string) (ScanJob, bool) {
|
func (s *MemoryStore) GetScan(id string) (ScanJob, bool, error) {
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
job, ok := s.jobs[id]
|
job, ok := s.jobs[id]
|
||||||
s.mu.RUnlock()
|
s.mu.RUnlock()
|
||||||
return job, ok
|
return job, ok, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MemoryStore) ListScans(limit int) []ScanJob {
|
func (s *MemoryStore) ListScans(limit int) []ScanJob {
|
||||||
|
|||||||
@@ -233,8 +233,16 @@
|
|||||||
|
|
||||||
async function fetchScanJob(id) {
|
async function fetchScanJob(id) {
|
||||||
const res = await fetch(`/api/scans/${encodeURIComponent(id)}`);
|
const res = await fetch(`/api/scans/${encodeURIComponent(id)}`);
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
|
const body = await res.text();
|
||||||
return res.json();
|
if (!res.ok) {
|
||||||
|
if (res.status === 503) {
|
||||||
|
throw new Error(
|
||||||
|
`Временно недоступно хранилище сканов (HTTP ${res.status}). Повторите через несколько секунд. ${body}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new Error(`HTTP ${res.status}: ${body}`);
|
||||||
|
}
|
||||||
|
return JSON.parse(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Просит сервер остановить скан (отмена воркера + статус canceled в БД). */
|
/** Просит сервер остановить скан (отмена воркера + статус canceled в БД). */
|
||||||
|
|||||||
Reference in New Issue
Block a user