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