use async_trait::async_trait;
use http::Extensions;
use reqwest::{Request, Response, Url};
use super::{Middleware, Next, Result};
pub const RELATIVE_MARKER_HOST: &str = "fetch-kit.relative";
#[derive(Debug, Clone)]
pub struct BaseUrlMiddleware {
base: Url,
}
impl BaseUrlMiddleware {
pub fn new(base: Url) -> Self {
Self { base }
}
pub fn relative(path: &str) -> Result<Url, url::ParseError> {
let trimmed = path.trim_start_matches('/');
Url::parse(&format!("http://{RELATIVE_MARKER_HOST}/{trimmed}"))
}
fn join(&self, relative: &Url) -> Url {
let req_path = relative.path().trim_start_matches('/');
let base_path = self.base.path().trim_end_matches('/');
let mut joined_url = self.base.clone();
joined_url.set_path(&format!("{base_path}/{req_path}"));
joined_url.set_query(relative.query());
joined_url
}
}
#[async_trait]
impl Middleware for BaseUrlMiddleware {
async fn handle(
&self,
mut req: Request,
extensions: &mut Extensions,
next: Next<'_>,
) -> Result<Response> {
if req.url().host_str() == Some(RELATIVE_MARKER_HOST) {
let joined = self.join(req.url());
*req.url_mut() = joined;
}
next.run(req, extensions).await
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)] use super::*;
fn mw(base: &str) -> BaseUrlMiddleware {
BaseUrlMiddleware::new(Url::parse(base).unwrap())
}
#[test]
fn relative_helper_builds_marker_host() {
let url = BaseUrlMiddleware::relative("/users/1").unwrap();
assert_eq!(url.host_str(), Some(RELATIVE_MARKER_HOST));
assert_eq!(url.path(), "/users/1");
}
#[test]
fn relative_helper_keeps_query() {
let url = BaseUrlMiddleware::relative("users?active=true").unwrap();
assert_eq!(url.path(), "/users");
assert_eq!(url.query(), Some("active=true"));
}
#[test]
fn join_prefixes_base_path() {
let middleware = mw("https://api.example.com/v1/");
let joined = middleware.join(&BaseUrlMiddleware::relative("/users/1").unwrap());
assert_eq!(joined.as_str(), "https://api.example.com/v1/users/1");
}
#[test]
fn join_normalizes_slashes() {
let middleware = mw("https://api.example.com");
let joined = middleware.join(&BaseUrlMiddleware::relative("users/1").unwrap());
assert_eq!(joined.as_str(), "https://api.example.com/users/1");
}
#[test]
fn join_preserves_query_and_replaces_base_query() {
let middleware = mw("https://api.example.com/v1?env=dev");
let joined = middleware.join(&BaseUrlMiddleware::relative("/users?q=2").unwrap());
assert_eq!(joined.as_str(), "https://api.example.com/v1/users?q=2");
assert_eq!(joined.query(), Some("q=2"));
}
#[test]
fn join_empty_path_lands_on_base_root() {
let middleware = mw("https://api.example.com/v1/");
let joined = middleware.join(&BaseUrlMiddleware::relative("").unwrap());
assert_eq!(joined.as_str(), "https://api.example.com/v1/");
}
#[test]
fn join_carries_base_credentials_and_port() {
let middleware = mw("http://user:pw@127.0.0.1:8080/api");
let joined = middleware.join(&BaseUrlMiddleware::relative("/x").unwrap());
assert_eq!(joined.as_str(), "http://user:pw@127.0.0.1:8080/api/x");
}
#[test]
fn relative_helper_percent_encodes_rather_than_panicking() {
let url = BaseUrlMiddleware::relative("a b/c?d=1").unwrap();
assert_eq!(url.path(), "/a%20b/c");
assert_eq!(url.query(), Some("d=1"));
}
}