Files
marktvogt.de/backend/internal/pkg/apierror/error.go
T
vikingowl adf417b731 fix(research): 429 aware error handling for Pass 1/2
Pass 1 and Pass 2 now detect Mistral web_search rate limits (shared with
the Pass 0 CronJob) and return a proper HTTP 429 with Retry-After: 60
instead of a generic 500 "AI research failed". Pass 2 is enrichment-only,
so rate-limits there fall through with pass1 results intact.

- pkg/ai: new shared IsRateLimit helper + DefaultRetryAfterSeconds=60.
  discovery/service.go drops its local copy and imports the shared one.
- apierror.TooManyRequests now accepts an optional custom message so the
  response body can include "try again in ~60s".
- market/research.go: respondRateLimited helper sets Retry-After,
  downgrades the log line from ERROR to WARN (rate-limits are expected
  state, not a fault), and returns 429 with a structured rate_limited
  code the admin UI can key off of.
2026-04-18 10:33:13 +02:00

90 lines
2.1 KiB
Go

package apierror
import (
"fmt"
"net/http"
)
type Error struct {
Status int `json:"-"`
Code string `json:"code"`
Message string `json:"message"`
}
func (e *Error) Error() string {
return fmt.Sprintf("[%d] %s: %s", e.Status, e.Code, e.Message)
}
type Response struct {
Error *ErrorBody `json:"error"`
}
type ErrorBody struct {
Code string `json:"code"`
Message string `json:"message"`
}
func NewResponse(e *Error) Response {
return Response{
Error: &ErrorBody{
Code: e.Code,
Message: e.Message,
},
}
}
func BadRequest(code, message string) *Error {
return &Error{Status: http.StatusBadRequest, Code: code, Message: message}
}
func Unauthorized(message string) *Error {
return &Error{Status: http.StatusUnauthorized, Code: "unauthorized", Message: message}
}
func Forbidden(message string) *Error {
return &Error{Status: http.StatusForbidden, Code: "forbidden", Message: message}
}
func NotFound(resource string) *Error {
return &Error{
Status: http.StatusNotFound,
Code: "not_found",
Message: fmt.Sprintf("%s not found", resource),
}
}
func Conflict(message string) *Error {
return &Error{Status: http.StatusConflict, Code: "conflict", Message: message}
}
// TooManyRequests accepts an optional custom message. Callers that want the
// default — "too many requests, please try again later" — pass no arguments;
// callers with context (e.g. "retry in ~60s") pass a single string.
func TooManyRequests(message ...string) *Error {
msg := "too many requests, please try again later"
if len(message) > 0 && message[0] != "" {
msg = message[0]
}
return &Error{
Status: http.StatusTooManyRequests,
Code: "rate_limited",
Message: msg,
}
}
func Internal(message string) *Error {
return &Error{
Status: http.StatusInternalServerError,
Code: "internal_error",
Message: message,
}
}
func Validation(message string) *Error {
return &Error{Status: http.StatusUnprocessableEntity, Code: "validation_error", Message: message}
}
func Gone(message string) *Error {
return &Error{Status: http.StatusGone, Code: "gone", Message: message}
}