adf417b731
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.
90 lines
2.1 KiB
Go
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}
|
|
}
|