Skip to main content

pagesift_api/
error.rs

1//! 错误码和 HTTP 状态码(PRD 6.5.2)。响应体沿用 Firecrawl 的 `{success: false, code, error}`,
2//! 状态码向 Firecrawl 的同类错误看齐,官方 SDK 抛出的异常类型因此不变(比如超时是 408)。
3
4use axum::Json;
5use axum::http::StatusCode;
6use axum::response::{IntoResponse, Response};
7use serde_json::{Value, json};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ErrorCode {
11    RobotsDisallowed,
12    AccessBarrier,
13    RateLimited,
14    HostCooldown,
15    /// 调用方超过了自己 API key 的每分钟请求数。
16    RateLimitExceeded,
17    /// 调用方同时进行的任务数到了上限。
18    ConcurrencyLimitExceeded,
19    /// 调用方当天的页面配额用完了。
20    QuotaExceeded,
21    Timeout,
22    SsrfBlocked,
23    TooLarge,
24    UpstreamError,
25    UnsupportedOption,
26    InvalidRequest,
27    Unauthorized,
28    NotFound,
29    Internal,
30    /// 域名解析失败:可以稍后重试,不同于 `SSRF_BLOCKED`(永远不许抓)。
31    DnsFailure,
32    /// 连接失败、被重置、TLS 握手失败,或者浏览器导航时的网络错误。
33    NetworkError,
34    /// 在请求的时限内等不到空闲的浏览器。
35    Busy,
36}
37
38impl ErrorCode {
39    pub fn as_str(self) -> &'static str {
40        match self {
41            ErrorCode::RobotsDisallowed => "ROBOTS_DISALLOWED",
42            ErrorCode::AccessBarrier => "ACCESS_BARRIER",
43            ErrorCode::RateLimited => "RATE_LIMITED",
44            ErrorCode::HostCooldown => "HOST_COOLDOWN",
45            ErrorCode::RateLimitExceeded => "RATE_LIMIT_EXCEEDED",
46            ErrorCode::ConcurrencyLimitExceeded => "CONCURRENCY_LIMIT_EXCEEDED",
47            ErrorCode::QuotaExceeded => "QUOTA_EXCEEDED",
48            ErrorCode::Timeout => "TIMEOUT",
49            ErrorCode::SsrfBlocked => "SSRF_BLOCKED",
50            ErrorCode::TooLarge => "TOO_LARGE",
51            ErrorCode::UpstreamError => "UPSTREAM_ERROR",
52            ErrorCode::UnsupportedOption => "UNSUPPORTED_OPTION",
53            ErrorCode::InvalidRequest => "INVALID_REQUEST",
54            ErrorCode::Unauthorized => "UNAUTHORIZED",
55            ErrorCode::NotFound => "NOT_FOUND",
56            ErrorCode::Internal => "INTERNAL_ERROR",
57            ErrorCode::DnsFailure => "DNS_FAILURE",
58            ErrorCode::NetworkError => "NETWORK_ERROR",
59            ErrorCode::Busy => "BUSY",
60        }
61    }
62
63    pub fn status(self) -> StatusCode {
64        match self {
65            ErrorCode::RobotsDisallowed | ErrorCode::AccessBarrier => StatusCode::FORBIDDEN,
66            ErrorCode::RateLimited
67            | ErrorCode::RateLimitExceeded
68            | ErrorCode::ConcurrencyLimitExceeded
69            | ErrorCode::QuotaExceeded => StatusCode::TOO_MANY_REQUESTS,
70            ErrorCode::HostCooldown | ErrorCode::Busy => StatusCode::SERVICE_UNAVAILABLE,
71            ErrorCode::Timeout => StatusCode::REQUEST_TIMEOUT,
72            ErrorCode::SsrfBlocked | ErrorCode::UnsupportedOption | ErrorCode::InvalidRequest => {
73                StatusCode::BAD_REQUEST
74            }
75            ErrorCode::TooLarge => StatusCode::PAYLOAD_TOO_LARGE,
76            ErrorCode::UpstreamError | ErrorCode::DnsFailure | ErrorCode::NetworkError => {
77                StatusCode::BAD_GATEWAY
78            }
79            ErrorCode::Unauthorized => StatusCode::UNAUTHORIZED,
80            ErrorCode::NotFound => StatusCode::NOT_FOUND,
81            ErrorCode::Internal => StatusCode::INTERNAL_SERVER_ERROR,
82        }
83    }
84}
85
86#[derive(Debug, Clone, PartialEq)]
87pub struct ApiError {
88    pub code: ErrorCode,
89    pub message: String,
90    /// 放进响应的 `details`,比如拦截页的厂商和置信度。
91    pub details: Option<Value>,
92    /// 429 时带上 `Retry-After`(秒)。
93    pub retry_after: Option<u64>,
94}
95
96impl ApiError {
97    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
98        Self {
99            code,
100            message: message.into(),
101            details: None,
102            retry_after: None,
103        }
104    }
105
106    pub fn with_details(mut self, details: Value) -> Self {
107        self.details = Some(details);
108        self
109    }
110
111    pub fn with_retry_after(mut self, secs: u64) -> Self {
112        self.retry_after = Some(secs);
113        self
114    }
115
116    pub fn unsupported(what: impl Into<String>) -> Self {
117        Self::new(ErrorCode::UnsupportedOption, what)
118    }
119
120    pub fn invalid(what: impl Into<String>) -> Self {
121        Self::new(ErrorCode::InvalidRequest, what)
122    }
123}
124
125impl std::fmt::Display for ApiError {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        write!(f, "{}: {}", self.code.as_str(), self.message)
128    }
129}
130
131impl std::error::Error for ApiError {}
132
133impl IntoResponse for ApiError {
134    fn into_response(self) -> Response {
135        let mut body = json!({
136            "success": false,
137            "code": self.code.as_str(),
138            "error": self.message,
139        });
140        if let Some(d) = self.details {
141            body["details"] = d;
142        }
143        let mut resp = (self.code.status(), Json(body)).into_response();
144        // 给监控中间件按错误码计数
145        resp.extensions_mut().insert(self.code);
146        if let Some(secs) = self.retry_after
147            && let Ok(v) = secs.to_string().parse()
148        {
149            resp.headers_mut().insert("retry-after", v);
150        }
151        resp
152    }
153}