firecrawl_sdk/
lib.rs

1use reqwest::{Client, Response};
2use serde::de::DeserializeOwned;
3use serde_json::Value;
4
5pub mod batch_scrape;
6pub mod crawl;
7pub mod document;
8mod error;
9pub mod map;
10pub mod scrape;
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                    success: false,
41                    error: "API key is required for cloud service".to_string(),
42                    details: None,
43                },
44            ));
45        }
46
47        Ok(FirecrawlApp {
48            api_key: api_key.map(|x| x.as_ref().to_string()),
49            api_url: url,
50            client: Client::new(),
51        })
52    }
53
54    fn prepare_headers(&self, idempotency_key: Option<&String>) -> reqwest::header::HeaderMap {
55        let mut headers = reqwest::header::HeaderMap::new();
56        headers.insert("Content-Type", "application/json".parse().unwrap());
57        if let Some(api_key) = self.api_key.as_ref() {
58            headers.insert(
59                "Authorization",
60                format!("Bearer {}", api_key).parse().unwrap(),
61            );
62        }
63        if let Some(key) = idempotency_key {
64            headers.insert("x-idempotency-key", key.parse().unwrap());
65        }
66        headers
67    }
68
69    async fn handle_response<'a, T: DeserializeOwned>(
70        &self,
71        response: Response,
72        action: impl AsRef<str>,
73    ) -> Result<T, FirecrawlError> {
74        let (is_success, status) = (response.status().is_success(), response.status());
75
76        let response = response
77            .text()
78            .await
79            .map_err(|e| FirecrawlError::ResponseParseErrorText(e))
80            .and_then(|response_json| {
81                serde_json::from_str::<Value>(&response_json)
82                    .map_err(|e| FirecrawlError::ResponseParseError(e))
83            })
84            .and_then(|response_value| {
85                if response_value["success"].as_bool().unwrap_or(false) {
86                    Ok(serde_json::from_value::<T>(response_value)
87                        .map_err(|e| FirecrawlError::ResponseParseError(e))?)
88                } else {
89                    Err(FirecrawlError::APIError(
90                        action.as_ref().to_string(),
91                        serde_json::from_value(response_value)
92                            .map_err(|e| FirecrawlError::ResponseParseError(e))?,
93                    ))
94                }
95            });
96
97        match &response {
98            Ok(_) => response,
99            Err(FirecrawlError::ResponseParseError(_))
100            | Err(FirecrawlError::ResponseParseErrorText(_)) => {
101                if is_success {
102                    response
103                } else {
104                    Err(FirecrawlError::HttpRequestFailed(
105                        action.as_ref().to_string(),
106                        status.as_u16(),
107                        status.as_str().to_string(),
108                    ))
109                }
110            }
111            Err(_) => response,
112        }
113    }
114}