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