44 * Uses auto-generated API client from OpenAPI spec for type safety.
55 */
66
7- use std:: { fs:: File , io:: Read , path:: Path } ;
7+ use std:: { collections :: HashMap , fs:: File , io:: Read , path:: Path } ;
88
99use crate :: api:: { types, Client , ClientUploadExt } ;
1010
@@ -44,20 +44,46 @@ pub const SERVER_BASE_URL: &str = "https://chattomap.com";
4444// Upload Implementation
4545// =============================================================================
4646
47- /// Create API client
48- fn create_client ( ) -> Client {
49- Client :: new ( SERVER_BASE_URL )
47+ /// Create API client with optional host override and custom headers
48+ fn create_client ( host_override : Option < & str > , custom_headers : & HashMap < String , String > ) -> Client {
49+ let base_url = host_override. unwrap_or ( SERVER_BASE_URL ) ;
50+
51+ if custom_headers. is_empty ( ) {
52+ return Client :: new ( base_url) ;
53+ }
54+
55+ // Build a reqwest client with custom headers
56+ let mut headers = reqwest:: header:: HeaderMap :: new ( ) ;
57+ for ( name, value) in custom_headers {
58+ if let ( Ok ( header_name) , Ok ( header_value) ) = (
59+ reqwest:: header:: HeaderName :: from_bytes ( name. as_bytes ( ) ) ,
60+ reqwest:: header:: HeaderValue :: from_str ( value) ,
61+ ) {
62+ headers. insert ( header_name, header_value) ;
63+ }
64+ }
65+
66+ let http_client = reqwest:: Client :: builder ( )
67+ . default_headers ( headers)
68+ . build ( )
69+ . unwrap_or_default ( ) ;
70+
71+ Client :: new_with_client ( base_url, http_client)
5072}
5173
5274/// Request a pre-signed upload URL from the server
53- pub async fn get_presigned_url ( ) -> Result < PresignResponse , String > {
54- let client = create_client ( ) ;
55-
56- let response = client
57- . upload_presign ( )
58- . send ( )
59- . await
60- . map_err ( |e| format ! ( "Failed to request presigned URL: {e}" ) ) ?;
75+ pub async fn get_presigned_url (
76+ host_override : Option < & str > ,
77+ custom_headers : & HashMap < String , String > ,
78+ ) -> Result < PresignResponse , String > {
79+ let client = create_client ( host_override, custom_headers) ;
80+
81+ let response = client. upload_presign ( ) . send ( ) . await . map_err ( |e| {
82+ format ! (
83+ "Failed to request presigned URL: {}" ,
84+ sanitize_api_error( & e)
85+ )
86+ } ) ?;
6187
6288 let body = response. into_inner ( ) ;
6389
@@ -121,8 +147,12 @@ pub async fn upload_file(
121147}
122148
123149/// Notify server that upload is complete and start processing
124- pub async fn complete_upload ( job_id : & str ) -> Result < CreateJobResponse , String > {
125- let client = create_client ( ) ;
150+ pub async fn complete_upload (
151+ job_id : & str ,
152+ host_override : Option < & str > ,
153+ custom_headers : & HashMap < String , String > ,
154+ ) -> Result < CreateJobResponse , String > {
155+ let client = create_client ( host_override, custom_headers) ;
126156
127157 let response = client
128158 . upload_complete ( )
@@ -131,7 +161,7 @@ pub async fn complete_upload(job_id: &str) -> Result<CreateJobResponse, String>
131161 } )
132162 . send ( )
133163 . await
134- . map_err ( |e| format ! ( "Failed to complete upload: {e}" ) ) ?;
164+ . map_err ( |e| format ! ( "Failed to complete upload: {}" , sanitize_api_error ( & e ) ) ) ?;
135165
136166 let body = response. into_inner ( ) ;
137167
@@ -146,14 +176,67 @@ pub async fn complete_upload(job_id: &str) -> Result<CreateJobResponse, String>
146176}
147177
148178/// Get the results page URL for a job
149- pub fn get_results_url ( job_id : & str ) -> String {
150- format ! ( "{}/processing/{}" , SERVER_BASE_URL , job_id)
179+ pub fn get_results_url ( job_id : & str , host_override : Option < & str > ) -> String {
180+ let base_url = host_override. unwrap_or ( SERVER_BASE_URL ) ;
181+ format ! ( "{}/processing/{}" , base_url, job_id)
151182}
152183
153184// =============================================================================
154185// Helper Functions
155186// =============================================================================
156187
188+ /// Sanitize API client errors (from progenitor)
189+ /// Extracts meaningful message from potentially HTML-heavy error responses
190+ fn sanitize_api_error < E : std:: fmt:: Display > ( error : & E ) -> String {
191+ let error_str = error. to_string ( ) ;
192+
193+ // Check if error contains HTML
194+ if error_str. contains ( "<!DOCTYPE" )
195+ || error_str. contains ( "<!doctype" )
196+ || error_str. contains ( "<html" )
197+ {
198+ // Try to extract a title from the HTML
199+ if let Some ( title) = extract_html_title ( & error_str) {
200+ return clean_error_text ( & title) ;
201+ }
202+ return "Server returned an HTML error page (authentication required?)" . to_string ( ) ;
203+ }
204+
205+ // For shorter errors, return as-is
206+ if error_str. len ( ) <= 200 {
207+ return error_str;
208+ }
209+
210+ // Truncate long errors
211+ format ! ( "{}..." , & error_str[ ..200 ] )
212+ }
213+
214+ /// Clean up error text - remove escaped bytes and non-ASCII characters
215+ fn clean_error_text ( text : & str ) -> String {
216+ // Remove escaped byte sequences like \xe3\x83\xbb
217+ let mut result = text. to_string ( ) ;
218+
219+ // Remove \xNN patterns
220+ while let Some ( pos) = result. find ( "\\ x" ) {
221+ if pos + 4 <= result. len ( ) {
222+ result = format ! ( "{}{}" , & result[ ..pos] , & result[ pos + 4 ..] ) ;
223+ } else {
224+ break ;
225+ }
226+ }
227+
228+ // Remove any remaining non-ASCII and normalize whitespace
229+ result
230+ . chars ( )
231+ . filter ( |c| {
232+ c. is_ascii_alphanumeric ( ) || c. is_ascii_whitespace ( ) || c. is_ascii_punctuation ( )
233+ } )
234+ . collect :: < String > ( )
235+ . split_whitespace ( )
236+ . collect :: < Vec < _ > > ( )
237+ . join ( " " )
238+ }
239+
157240/// Sanitize an error response body for display
158241///
159242/// If the body looks like HTML, extract a meaningful message or return a generic error.
@@ -247,11 +330,17 @@ mod tests {
247330
248331 #[ test]
249332 fn test_get_results_url ( ) {
250- let url = get_results_url ( "abc123" ) ;
333+ let url = get_results_url ( "abc123" , None ) ;
251334 assert ! ( url. contains( "abc123" ) ) ;
252335 assert ! ( url. contains( "/processing/" ) ) ;
253336 }
254337
338+ #[ test]
339+ fn test_get_results_url_with_override ( ) {
340+ let url = get_results_url ( "abc123" , Some ( "http://localhost:5173" ) ) ;
341+ assert_eq ! ( url, "http://localhost:5173/processing/abc123" ) ;
342+ }
343+
255344 #[ test]
256345 fn test_sanitize_error_body_empty ( ) {
257346 assert_eq ! ( sanitize_error_body( "" ) , "(empty response)" ) ;
0 commit comments