#Reqwest to axum backend API failing on web but working on desktop

1 messages · Page 1 of 1 (latest)

sick pendant
#

I'm using axum for my backend as I want the base url to be configurable so my desktop builds use an external backend. It works fine on desktop and my config is like below:

// Create API routes (unchanged)
    let api_routes = Router::new()
        .route("/user/{id}", get(api_get_user))
        .route("/signin", post(api_try_signin))
        .route("/verify-otp", post(api_verify_otp))
        .route("/signup", post(api_try_signup));

    // Add CORS layer to allow cross-origin requests (adjust as needed)
    let cors = CorsLayer::new()
        .allow_origin(Any) // Permits all origins (restrict to specific domains for security)
        .allow_methods(Any)
        .allow_headers(Any);

    let router = axum::Router::new()
        .nest("/api", api_routes)
        .layer(cors) // Apply CORS before Dioxus app
        .serve_dioxus_application(ServeConfig::new().unwrap(), App);

    let router = router.into_make_service();

This works fine on desktop. I can retrieve data from routes with the utils in this snippet:

#

impl ApiService {
    pub fn new(base_url: String) -> Self {
        Self {
            base_url: base_url.trim_end_matches('/').to_string(),
            client: reqwest::Client::new(),
        }
    }

    // Get global instance
    pub fn global() -> &'static ApiService {
        API_SERVICE.get_or_init(|| ApiService::new(get_api_base_url()))
    }

    // Helper method to make GET requests
    async fn get<T>(&self, endpoint: &str) -> Result<T, ApiError>
    where
        T: for<'de> Deserialize<'de>,
    {
        let url = format!("{}/api{}", self.base_url, endpoint);
        println!("Making GET request to: {}", url);
        let response = self.client.get(&url).send().await?;

        if !response.status().is_success() {
            return Err(ApiError::Api(format!(
                "HTTP {}: {}",
                response.status(),
                response.text().await.unwrap_or_default()
            )));
        }

        let api_response: ApiResponse<T> = response.json().await?;

        if !api_response.success {
            return Err(ApiError::Api(
                api_response
                    .message
                    .unwrap_or_else(|| "Unknown API error".to_string()),
            ));
        }

        api_response
            .data
            .ok_or_else(|| ApiError::Api("No data in successful response".to_string()))
    }
    // ...
}

However on web the request fails completely when triggered and it refreshes the current page (when it's not meant to).

The server logs show the following when normally it displays the api routes requested:

00:56:28 [server] [200] /?
00:56:37 [server] [200] /?
00:56:44 [server] [200] /?
00:56:48 [server] [200] /?
00:56:53 [server] [200] /?

I get the error: Error: Request failed: error sending request

#

For getting the base url for API requests I use this logic:

pub fn get_api_base_url() -> String {
    if is_development() {
        format!(
            "http://{}",
            dioxus::cli_config::fullstack_address_or_localhost()
        )
    } else {
        std::env::var("BACKEND_URL").expect("BACKEND_URL not set")
    }
}```


I am pretty confused to why it's not working. I thought it was CORS but I think it may be an issue with API route passthrough (like when requesting /api it doesn't go to the backend and just fails).

Any ideas would be appreciated.
sick pendant
#

Bump

clear ruin
#

i will show snippets of how i did my axum backend with dioxus web only.

Perhaps it will help you:

my .env in the backend as the docker .env will overrule it:
BACKEND_HOST="127.0.0.1"
BACKEND_PORT="3000"

#

Backend main:
to_env().ok();

let host_str = env::var("BACKEND_HOST").unwrap_or_else(|| "127.0.0.1".to_string());
let port_str = env::var("BACKEND_PORT").unwrap_or_else(|
| "3000".to_string());

let host: IpAddr = host_str.parse()
    .with_context(|| format!("Invalid IP address format for BACKEND_HOST: {}", host_str))?;
let port: u16 = port_str.parse()
    .with_context(|| format!("BACKEND_PORT must be a valid number: {}", port_str))?;

let addr = SocketAddr::new(host, port);

/// The frontend can use this to check if the server is online.
pub async fn health_check() -> impl IntoResponse {
StatusCode::OK
}

#

Frontend:

#[derive(Debug)]
struct UrlError(String);

#[cfg(feature = "dev")]
fn get_api_url(path: &str) -> Result<String, UrlError> {
    // For development, we always point to the backend server on port 3000.
    web_sys::console::log_1(&"Using DEV url".into()); 
    let base_url = "http://127.0.0.1:3000";
    Ok(format!("{base_url}{path}"))
}
#

#[cfg(not(feature = "dev"))]
fn get_api_url(path: &str) -> Result<String, UrlError> {
let window = web_sys::window()
.ok_or_else(|| UrlError("web-sys: Could not get the global window object.".to_string()))?;

    let location = window.location();

    let protocol = location.protocol().map_err(|e| UrlError(format!("web-sys: Failed to get protocol: {:?}", e)))?;
    let hostname = location.hostname().map_err(|e| UrlError(format!("web-sys: Failed to get hostname: {:?}", e)))?;
    let port = location.port().map_err(|e| UrlError(format!("web-sys: Failed to get port: {:?}", e)))?;

    // Reconstruct the base URL.
    let base_url = if port.is_empty() {
        format!("{protocol}//{hostname}")
    } else {
        format!("{protocol}//{hostname}:{port}")
    };

    // Check for "null" hostname.
    if hostname == "null" || hostname.is_empty() {
        return Err(UrlError(
            "The hostname is invalid. This can happen if you open the HTML file directly. Please use a web server.".to_string()
        ));
    }
    
    Ok(format!("{base_url}{path}"))
}

/// Helper to convert our internal UrlError into a user-facing ErrorResponse.
fn url_error_to_response(err: UrlError) -> ErrorResponse {
    web_sys::console::error_1(&err.0.into());
    ErrorResponse {
        error_code: "WEB-901-003".to_string(),
        message: "Kon de server-URL niet bepalen.".to_string(),
    }
}
#

The function that makes the check in the backend:

pub async fn check_server_health() -> Result<(), ErrorResponse> {
    let client = reqwest::Client::new();
    let health_url = get_api_url("/api/health").map_err(url_error_to_response)?;

    match client.get(health_url).send().await {
        Ok(response) if response.status().is_success() => Ok(()),
        _ => Err(ErrorResponse {
            error_code: "WEB-901-001".to_string(),
            message: "Kon geen verbinding maken met de server.".to_string(),
        }),
    }
}

#

The cargo.toml in my workspace of the frontend:

[features]
dev = []


This setup works for docker and development however you call it like this:
dx serve --platform web --features dev

#

I want to stress that:
let host_str = env::var("BACKEND_HOST").unwrap_orelse(|| "127.0.0.1".to_string());
let port_str = env::var("BACKEND_PORT").unwrap_orelse(|| "3000".to_string());

Is not the right way to approach it and should be done cleaner