• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

divviup / divviup-api / 25528952740

08 May 2026 12:06AM UTC coverage: 57.286% (-1.3%) from 58.564%
25528952740

Pull #2247

github

web-flow
Merge 6f819cf00 into 91680da33
Pull Request #2247: Migrate from Trillium [part 7C]: admin routes and Trillium router removal

42 of 63 new or added lines in 5 files covered. (66.67%)

111 existing lines in 12 files now uncovered.

4352 of 7597 relevant lines covered (57.29%)

64.42 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.6
/src/handler/proxy.rs
1
//! Temporary reverse proxy handler that forwards unmatched Trillium requests to
2
//! the local Axum server. This exists only during the incremental migration and
3
//! will be removed once all routes have been moved to Axum.
4

5
use reqwest::{
6
    header::{HeaderName, CONNECTION, HOST, TE, TRAILER, TRANSFER_ENCODING},
7
    redirect,
8
};
9
use std::net::SocketAddr;
10
use trillium::{Conn, Handler, Status};
11

12
/// A Trillium [`Handler`] that proxies unhalted requests to a local Axum server.
13
#[derive(Debug)]
14
pub struct AxumProxy {
15
    upstream: String,
16
    client: reqwest::Client,
17
}
18

19
impl AxumProxy {
20
    pub fn new(addr: SocketAddr) -> Self {
278✔
21
        Self {
278✔
22
            upstream: format!("http://[::1]:{}", addr.port()),
278✔
23
            client: reqwest::Client::builder()
278✔
24
                .no_proxy()
278✔
25
                .redirect(redirect::Policy::none())
278✔
26
                .build()
278✔
27
                .expect("failed to build proxy HTTP client"),
278✔
28
        }
278✔
29
    }
278✔
30
}
31

32
/// Hop-by-hop headers that should not be forwarded through the proxy (RFC 7230 ยง6.1).
33
const UNPROXYABLE_HEADERS: [HeaderName; 5] = [HOST, TRANSFER_ENCODING, CONNECTION, TE, TRAILER];
34

35
#[trillium::async_trait]
36
impl Handler for AxumProxy {
37
    async fn run(&self, mut conn: Conn) -> Conn {
546✔
38
        // Only proxy requests that haven't been handled by earlier handlers.
39
        if conn.status().is_some() || conn.is_halted() {
273✔
UNCOV
40
            return conn;
×
41
        }
273✔
42

43
        let method = conn.method();
273✔
44
        let path = conn.path();
273✔
45
        let querystring = conn.querystring();
273✔
46

47
        let url = if querystring.is_empty() {
273✔
48
            format!("{}{}", self.upstream, path)
268✔
49
        } else {
50
            format!("{}{}?{}", self.upstream, path, querystring)
5✔
51
        };
52

53
        let reqwest_method = match reqwest::Method::from_bytes(method.as_ref().as_bytes()) {
273✔
54
            Ok(m) => m,
273✔
55
            Err(_) => return conn.with_status(Status::BadRequest).halt(),
×
56
        };
57

58
        let mut builder = self.client.request(reqwest_method, &url);
273✔
59

60
        // Forward request headers, filtering out hop-by-hop headers.
61
        for (name, values) in conn.request_headers() {
1,219✔
62
            let header_name = match HeaderName::from_bytes(name.as_ref().as_bytes()) {
1,219✔
63
                Ok(h) => h,
1,219✔
64
                Err(_) => continue,
×
65
            };
66
            if UNPROXYABLE_HEADERS.contains(&header_name) {
1,219✔
67
                continue;
273✔
68
            }
946✔
69
            for value in values.iter() {
946✔
70
                if let Some(s) = value.as_str() {
946✔
71
                    builder = builder.header(&header_name, s);
946✔
72
                }
946✔
73
            }
74
        }
75

76
        // Forward the request body. Note: no size limit is enforced here;
77
        // the Trillium API layer (trillium-api) enforces a 1 MiB limit before
78
        // requests reach this handler, so it's fine for the migration window.
79
        let body = conn.request_body().await.read_bytes().await;
273✔
80
        match body {
273✔
81
            Ok(bytes) if !bytes.is_empty() => {
273✔
82
                builder = builder.body(bytes);
110✔
83
            }
110✔
84
            Err(e) => {
×
85
                log::error!("axum proxy error reading request body: {e}");
×
86
                return conn.with_status(Status::BadRequest).halt();
×
87
            }
88
            _ => {}
163✔
89
        }
90

91
        let resp = match builder.send().await {
273✔
92
            Ok(resp) => resp,
273✔
93
            Err(e) => {
×
94
                log::error!("axum proxy error: {e}");
×
95
                return conn.with_status(Status::BadGateway).halt();
×
96
            }
97
        };
98

99
        let status = resp.status().as_u16();
273✔
100
        let resp_headers = resp.headers().clone();
273✔
101
        let body = match resp.bytes().await {
273✔
102
            Ok(b) => b,
273✔
103
            Err(e) => {
×
104
                log::error!("axum proxy error reading response: {e}");
×
105
                return conn.with_status(Status::BadGateway).halt();
×
106
            }
107
        };
108

109
        let mut conn = conn.with_status(status).halt();
273✔
110

111
        for (name, value) in resp_headers.iter() {
2,041✔
112
            if UNPROXYABLE_HEADERS.contains(name) {
2,041✔
113
                continue;
×
114
            }
2,041✔
115
            if let Ok(v) = value.to_str() {
2,041✔
116
                conn.response_headers_mut()
2,041✔
117
                    .append(name.as_str().to_owned(), v.to_owned());
2,041✔
118
            }
2,041✔
119
        }
120

121
        // This copies the response body; we could avoid it by streaming via
122
        // resp.into_body() + Body::new_streaming, but it's not worth the extra
123
        // plumbing for a temporary shim.
124
        conn.with_body(body.to_vec())
273✔
125
    }
546✔
126
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc