codoseo_web/agent/
error.rs1use axum::Json;
5use axum::http::{HeaderValue, StatusCode, header};
6use axum::response::{IntoResponse, Response};
7use serde_json::json;
8
9pub const SITE_NOT_FOUND: &str = "No such site. List your sites to see their ids.";
12
13#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
14pub enum AgentError {
15 #[error("{0}")]
17 Unauthorized(String),
18 #[error("{0}")]
19 NotFound(String),
20 #[error("{0}")]
21 BadRequest(String),
22 #[error("{0}")]
24 MethodNotAllowed(String),
25 #[error("{0}")]
27 CrawlInProgress(String),
28 #[error("{0}")]
30 PlanLimit(String),
31 #[error("You have used all {limit} API calls for today. The count starts again at 00:00 UTC.")]
33 QuotaExceeded { limit: u32, retry_after_secs: u64 },
34 #[error("CodoSEO can't reach its database right now. Try again in a moment.")]
36 Unavailable,
37 #[error("Something went wrong on our side. Try again in a moment.")]
38 Internal(String),
39}
40
41impl AgentError {
42 pub fn status(&self) -> StatusCode {
43 match self {
44 AgentError::Unauthorized(_) => StatusCode::UNAUTHORIZED,
45 AgentError::NotFound(_) => StatusCode::NOT_FOUND,
46 AgentError::BadRequest(_) => StatusCode::BAD_REQUEST,
47 AgentError::MethodNotAllowed(_) => StatusCode::METHOD_NOT_ALLOWED,
48 AgentError::CrawlInProgress(_) => StatusCode::CONFLICT,
49 AgentError::PlanLimit(_) => StatusCode::FORBIDDEN,
50 AgentError::QuotaExceeded { .. } => StatusCode::TOO_MANY_REQUESTS,
51 AgentError::Unavailable => StatusCode::SERVICE_UNAVAILABLE,
52 AgentError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
53 }
54 }
55
56 pub fn code(&self) -> &'static str {
58 match self {
59 AgentError::Unauthorized(_) => "unauthorized",
60 AgentError::NotFound(_) => "not_found",
61 AgentError::BadRequest(_) | AgentError::MethodNotAllowed(_) => "bad_request",
62 AgentError::CrawlInProgress(_) => "crawl_in_progress",
63 AgentError::PlanLimit(_) => "plan_limit",
64 AgentError::QuotaExceeded { .. } => "quota_exceeded",
65 AgentError::Unavailable => "unavailable",
66 AgentError::Internal(_) => "internal",
67 }
68 }
69
70 pub fn message(&self) -> String {
71 self.to_string()
72 }
73
74 pub fn site_not_found() -> AgentError {
75 AgentError::NotFound(SITE_NOT_FOUND.to_owned())
76 }
77}
78
79impl From<sqlx::Error> for AgentError {
80 fn from(e: sqlx::Error) -> AgentError {
81 if crate::error::is_unavailable(&e) {
82 AgentError::Unavailable
83 } else {
84 AgentError::Internal(e.to_string())
85 }
86 }
87}
88
89impl IntoResponse for AgentError {
91 fn into_response(self) -> Response {
92 if let AgentError::Internal(detail) = &self {
93 tracing::error!(%detail, "api request failed");
94 }
95 let body = json!({ "error": { "code": self.code(), "message": self.message() } });
96 let mut res = (self.status(), Json(body)).into_response();
97 let h = res.headers_mut();
98 h.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
99 match &self {
100 AgentError::Unauthorized(_) => {
101 h.insert(header::WWW_AUTHENTICATE, HeaderValue::from_static("Bearer"));
102 }
103 AgentError::QuotaExceeded {
104 limit,
105 retry_after_secs,
106 } => {
107 h.insert(header::RETRY_AFTER, HeaderValue::from(*retry_after_secs));
108 h.insert("x-ratelimit-limit", HeaderValue::from(*limit));
109 h.insert("x-ratelimit-remaining", HeaderValue::from(0u32));
110 }
111 AgentError::Unavailable => {
112 h.insert(header::RETRY_AFTER, HeaderValue::from_static("10"));
113 }
114 _ => {}
115 }
116 res
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn a_missing_row_is_our_bug_not_a_missing_site() {
126 let e = AgentError::from(sqlx::Error::RowNotFound);
127 assert_eq!(e.code(), "internal");
128 assert_eq!(e.status(), StatusCode::INTERNAL_SERVER_ERROR);
129 assert!(!e.message().contains("site"));
130 }
131}