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 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 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}