firecrawl_sdk/
lib.rs

1use reqwest::{Client, Response};
2use serde::de::DeserializeOwned;
3
4pub mod batch_scrape;
5pub mod crawl;
6pub mod document;
7mod error;
8pub mod map;
9pub mod scrape;
10pub mod search;
11
12use error::FirecrawlAPIError;
13pub use error::FirecrawlError;
14
15#[derive(Clone, Debug)]
16pub struct FirecrawlApp {
17    api_key: Option<String>,
18    api_url: String,
19    client: Client,
20}
21
22pub(crate) const API_VERSION: &str = "v1";
23const CLOUD_API_URL: &str = "https://api.firecrawl.dev";
24
25impl FirecrawlApp {
26    pub fn new(api_key: impl AsRef<str>) -> Result<Self, FirecrawlError> {
27        FirecrawlApp::new_selfhosted(CLOUD_API_URL, Some(api_key))
28    }
29
30    pub fn new_with_client(
31        api_key: impl AsRef<str>,
32        client: Client,
33    ) -> Result<Self, FirecrawlError> {
34        Ok(FirecrawlApp {
35            api_key: Some(api_key.as_ref().to_string()),
36            api_url: CLOUD_API_URL.to_string(),
37            client,
38        })
39    }
40
41    pub fn new_selfhosted(
42        api_url: impl AsRef<str>,
43        api_key: Option<impl AsRef<str>>,
44    ) -> Result<Self, FirecrawlError> {
45        FirecrawlApp::new_selfhosted_with_client(api_url, api_key, Client::new())
46    }
47
48    pub fn new_selfhosted_with_client(
49        api_url: impl AsRef<str>,
50        api_key: Option<impl AsRef<str>>,
51        client: Client,
52    ) -> Result<Self, FirecrawlError> {
53        let url = api_url.as_ref().to_string();
54
55        if url == CLOUD_API_URL && api_key.is_none() {
56            return Err(FirecrawlError::APIError(
57                "Configuration".to_string(),
58                FirecrawlAPIError {
59                    error: "API key is required for cloud service".to_string(),
60                    details: None,
61                },
62            ));
63        }
64
65        Ok(FirecrawlApp {
66            api_key: api_key.map(|x| x.as_ref().to_string()),
67            api_url: url,
68            client: Client::new(),
69        })
70    }
71
72    fn prepare_headers(&self, idempotency_key: Option<&String>) -> reqwest::header::HeaderMap {
73        let mut headers = reqwest::header::HeaderMap::new();
74        headers.insert("Content-Type", "application/json".parse().unwrap());
75        if let Some(api_key) = self.api_key.as_ref() {
76            headers.insert(
77                "Authorization",
78                format!("Bearer {}", api_key).parse().unwrap(),
79            );
80        }
81        if let Some(key) = idempotency_key {
82            headers.insert("x-idempotency-key", key.parse().unwrap());
83        }
84        headers
85    }
86
87    async fn handle_response<T: DeserializeOwned>(
88        &self,
89        response: Response,
90        action: impl AsRef<str>,
91    ) -> Result<T, FirecrawlError> {
92        let status = response.status();
93
94        if !status.is_success() {
95            // For non-successful status codes, try to extract error details
96            match response.json::<FirecrawlAPIError>().await {
97                Ok(api_error) => {
98                    return Err(FirecrawlError::APIError(
99                        action.as_ref().to_string(),
100                        api_error,
101                    ));
102                }
103                Err(_) => {
104                    return Err(FirecrawlError::HttpRequestFailed(
105                        action.as_ref().to_string(),
106                        status.as_u16(),
107                        status.as_str().to_string(),
108                    ));
109                }
110            }
111        }
112
113        // For successful responses, directly deserialize to T
114        response.json::<T>().await.map_err(|e| {
115            if e.is_decode() {
116                FirecrawlError::ResponseParseErrorText(e)
117            } else {
118                FirecrawlError::HttpError(action.as_ref().to_string(), e)
119            }
120        })
121    }
122}