[refactor](alarm): make a readable message structural on every error

An error status with an empty body is a client-breaking bug that review does not reliably catch. Route every JSON and plain-text error through internal/apierr, which substitutes the code canonical message when a caller supplies none, and keep the frozen codes in one package shared by the action and WebSocket layers.
This commit is contained in:
2026-10-19 02:26:46 +01:00
parent 3ccea3e9f6
commit 028f9e7dae
5 changed files with 522 additions and 35 deletions
+14 -32
View File
@@ -31,7 +31,6 @@ import (
"time"
"git.dcntrlized.org/Dcntrlized/Alarm/db"
"git.dcntrlized.org/Dcntrlized/Alarm/internal/alertstate"
"git.dcntrlized.org/Dcntrlized/Alarm/internal/apierr"
"git.dcntrlized.org/dcntrlized/dcntrlized-auth/authclient"
"github.com/google/uuid"
@@ -40,15 +39,16 @@ import (
// Frozen v1 error codes, aliased from internal/apierr so the WebSocket layer
// (which cannot import this package) uses the same strings.
const (
CodeAlertNotFound = apierr.AlertNotFound
CodeAlertTokenInvalid = apierr.AlertTokenInvalid
CodeAlertNotAdmin = apierr.AlertNotAdmin
CodeAlertAlreadyJoined = apierr.AlertAlreadyJoined
CodeAlertNotSubscribed = apierr.AlertNotSubscribed
CodeAlertState = apierr.AlertState
CodeValidation = apierr.Validation
CodeUnauthorized = apierr.Unauthorized
CodeInternal = apierr.Internal
CodeAlertNotFound = apierr.AlertNotFound
CodeAlertTokenInvalid = apierr.AlertTokenInvalid
CodeAlertNotAdmin = apierr.AlertNotAdmin
CodeAlertAlreadyJoined = apierr.AlertAlreadyJoined
CodeAlertNotSubscribed = apierr.AlertNotSubscribed
CodeAlertState = apierr.AlertState
CodeAlarmTriggeredOffline = apierr.AlarmTriggeredOffline
CodeValidation = apierr.Validation
CodeUnauthorized = apierr.Unauthorized
CodeInternal = apierr.Internal
)
// maxMessageBytes / maxNameChars bound the free-text fields.
@@ -60,22 +60,11 @@ const (
maxPlatformLen = 32
)
// errorEnvelope is the platform error body.
type errorEnvelope struct {
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
// writeError emits {"error":{"code","message"}}.
// writeError emits {"error":{"code","message"}}. It delegates to apierr.Write,
// which guarantees both fields are non-empty: this package can pass a literal
// message, but the invariant is enforced centrally (see internal/apierr).
func writeError(w http.ResponseWriter, status int, code, message string) {
var body errorEnvelope
body.Error.Code = code
body.Error.Message = message
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(body)
apierr.Write(w, status, code, message)
}
// writeJSON emits a success body.
@@ -180,13 +169,6 @@ func validateMessage(raw string) (string, error) {
return raw, nil
}
func validateAutoStop(seconds int) error {
if seconds < 0 || seconds > alertstate.MaxAutoStopSeconds {
return fmt.Errorf("autoStopSeconds must be between 0 and %d", alertstate.MaxAutoStopSeconds)
}
return nil
}
func validateDeviceID(raw string) (string, error) {
id := strings.TrimSpace(raw)
if id == "" {