Files
minstrel/internal/apierror/apierror.go
T
bvandeusen 58766dbebe fix(server/api): preserve 500-class messages via apierror.InternalMsg
The PR1-T2 admin handler migration replaced bespoke 500-class messages
("lookup failed", "refetch failed", "trigger failed", "bump failed",
"remove failed", "update failed", "schedule row missing", "re-read
failed", "read failed") with apierror.Internal(err), which forces the
wire Message to "internal server error". That violates the PR1
contract that errEnvelope{Code, Message} must remain byte-identical
for existing clients.

Add apierror.InternalMsg(message, cause) — a 500-class constructor
that preserves the call site's user-facing message while keeping the
cause attached for logging and errors.Is. Re-migrate the 12 admin
sites whose pre-1cc7eb6 source had a non-empty bespoke Message so
they restore the original wire string. Sites whose original Message
was empty stay on Internal(err) (humanization to "internal server
error" is a tolerable improvement, not a regression).

admin_smtp.go's send_failed site (line 129) was already preserved
via a raw &apierror.Error literal in 1cc7eb6 and needs no fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:10:18 -04:00

74 lines
1.9 KiB
Go

package apierror
import (
"errors"
"fmt"
)
type Error struct {
Status int
Code string
Message string
Cause error
}
func (e *Error) Error() string {
if e.Cause != nil {
return fmt.Sprintf("%s: %v", e.Message, e.Cause)
}
return e.Message
}
func (e *Error) Unwrap() error { return e.Cause }
// From returns err as *Error if it (or anything in its chain) is one;
// otherwise wraps err in an Internal.
func From(err error) *Error {
if err == nil {
return nil
}
var apiErr *Error
if errors.As(err, &apiErr) {
return apiErr
}
return Internal(err)
}
func NotFound(what string) *Error {
return &Error{Status: 404, Code: what + "_not_found", Message: what + " not found"}
}
func BadRequest(code, message string) *Error {
return &Error{Status: 400, Code: code, Message: message}
}
func Conflict(code, message string) *Error {
return &Error{Status: 409, Code: code, Message: message}
}
func Forbidden(code, message string) *Error {
return &Error{Status: 403, Code: code, Message: message}
}
func Unauthorized(code, message string) *Error {
return &Error{Status: 401, Code: code, Message: message}
}
func Internal(cause error) *Error {
return &Error{Status: 500, Code: "server_error", Message: "internal server error", Cause: cause}
}
// InternalMsg returns a 500 Error with a custom user-facing message
// and a cause for logging. Use when the call site has a meaningful
// message that should appear on the wire (e.g. "lookup failed") rather
// than the generic "internal server error" from Internal.
func InternalMsg(message string, cause error) *Error {
return &Error{Status: 500, Code: "server_error", Message: message, Cause: cause}
}
var (
ErrNotFound = &Error{Status: 404, Code: "not_found", Message: "not found"}
ErrForbidden = &Error{Status: 403, Code: "forbidden", Message: "forbidden"}
ErrUnauthorized = &Error{Status: 401, Code: "unauthorized", Message: "authentication required"}
)