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        let url = api_url.as_ref().to_string();
46
47        if url == CLOUD_API_URL && api_key.is_none() {
48            return Err(FirecrawlError::APIError(
49                "Configuration".to_string(),
50                FirecrawlAPIError {
51                    error: "API key is required for cloud service".to_string(),
52                    details: None,
53                },
54            ));
55        }
56
57        Ok(FirecrawlApp {
58            api_key: api_key.map(|x| x.as_ref().to_string()),
59            api_url: url,
60            client: Client::new(),
61        })
62    }
63
64    fn prepare_headers(&self, idempotency_key: Option<&String>) -> reqwest::header::HeaderMap {
65        let mut headers = reqwest::header::HeaderMap::new();
66        headers.insert("Content-Type", "application/json".parse().unwrap());
67        if let Some(api_key) = self.api_key.as_ref() {
68            headers.insert(
69                "Authorization",
70                format!("Bearer {}", api_key).parse().unwrap(),
71            );
72        }
73        if let Some(key) = idempotency_key {
74            headers.insert("x-idempotency-key", key.parse().unwrap());
75        }
76        headers
77    }
78
79    async fn handle_response<T: DeserializeOwned>(
80        &self,
81        response: Response,
82        action: impl AsRef<str>,
83    ) -> Result<T, FirecrawlError> {
84        let status = response.status();
85
86        if !status.is_success() {
87            // For non-successful status codes, try to extract error details
88            match response.json::<FirecrawlAPIError>().await {
89                Ok(api_error) => {
90                    return Err(FirecrawlError::APIError(
91                        action.as_ref().to_string(),
92                        api_error,
93                    ));
94                }
95                Err(_) => {
96                    return Err(FirecrawlError::HttpRequestFailed(
97                        action.as_ref().to_string(),
98                        status.as_u16(),
99                        status.as_str().to_string(),
100                    ));
101                }
102            }
103        }
104
105        // For successful responses, directly deserialize to T
106        response.json::<T>().await.map_err(|e| {
107            if e.is_decode() {
108                FirecrawlError::ResponseParseErrorText(e)
109            } else {
110                FirecrawlError::HttpError(action.as_ref().to_string(), e)
111            }
112        })
113    }
114}