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

divviup / divviup-api / 24378279002

14 Apr 2026 02:48AM UTC coverage: 57.449% (+0.3%) from 57.169%
24378279002

Pull #2197

github

web-flow
Merge 05c5b3cb4 into de71dd7db
Pull Request #2197: Migrate from Trillium [part 2]: Axum scaffold and proxy fallback

77 of 95 new or added lines in 2 files covered. (81.05%)

3 existing lines in 2 files now uncovered.

4153 of 7229 relevant lines covered (57.45%)

60.91 hits per line

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

90.91
/src/handler.rs
1
pub(crate) mod account_bearer_token;
2
#[cfg(assets)]
3
pub(crate) mod assets;
4
pub(crate) mod cors;
5
pub(crate) mod custom_mime_types;
6
pub(crate) mod error;
7
pub(crate) mod logger;
8
pub(crate) mod misc;
9
pub(crate) mod oauth2;
10
pub(crate) mod opentelemetry;
11
pub(crate) mod origin_router;
12
pub(crate) mod session_store;
13

14
pub(crate) mod proxy;
15

16
use crate::{routes, Config, Db};
17

18
use axum::extract::DefaultBodyLimit;
19
use cors::cors_headers;
20
use error::ErrorHandler;
21
use logger::logger;
22
use proxy::AxumProxy;
23
use session_store::SessionStore;
24
use std::{borrow::Cow, net::Ipv6Addr, net::SocketAddr, sync::Arc};
25
use tokio::net::TcpListener;
26
use tower_http::trace::TraceLayer;
27
use trillium::{state, Handler, Info};
28
use trillium_caching_headers::{
29
    cache_control, caching_headers,
30
    CacheControlDirective::{MustRevalidate, Private},
31
};
32
use trillium_compression::compression;
33
use trillium_conn_id::conn_id;
34
use trillium_cookies::cookies;
35
use trillium_forwarding::Forwarding;
36
use trillium_macros::Handler;
37
use trillium_sessions::sessions;
38

39
pub(crate) use custom_mime_types::ReplaceMimeTypes;
40
pub(crate) use misc::*;
41

42
pub use error::Error;
43
pub use origin_router::origin_router;
44

45
use self::opentelemetry::opentelemetry;
46

47
#[cfg(feature = "otlp-trace")]
48
use trillium_opentelemetry::global::instrument_handler;
49
#[cfg(not(feature = "otlp-trace"))]
50
fn instrument_handler(handler: impl Handler) -> impl Handler {
1,946✔
51
    handler
1,946✔
52
}
1,946✔
53

54
/// Shared state for the Axum side of the application during migration.
55
#[derive(Clone, Debug)]
56
pub struct AxumAppState {
57
    pub db: Db,
58
    pub config: Arc<Config>,
59
}
60

61
#[derive(Handler, Debug)]
62
pub struct DivviupApi {
63
    #[handler(except = init)]
64
    handler: Box<dyn Handler>,
65
    db: Db,
66
    config: Arc<Config>,
67
    #[handler(skip)]
68
    axum_addr: SocketAddr,
69
}
70

71
impl DivviupApi {
72
    pub async fn init(&mut self, info: &mut Info) {
278✔
73
        *info.server_description_mut() = format!("divviup-api {}", env!("CARGO_PKG_VERSION"));
278✔
74
        *info.listener_description_mut() = format!(
278✔
75
            "api url: {}\n             app url: {}\n             axum: {}\n",
76
            self.config.api_url, self.config.app_url, self.axum_addr,
278✔
77
        );
78
        self.handler.init(info).await
278✔
79
    }
278✔
80

81
    pub async fn new(config: Config) -> Self {
278✔
82
        let config = Arc::new(config);
278✔
83
        let db = Db::connect(config.database_url.as_ref()).await;
278✔
84

85
        // Spawn the Axum server on an ephemeral port. Routes will be migrated
86
        // here incrementally; for now the router is empty and the proxy below
87
        // is a no-op fallback.
88
        let axum_state = AxumAppState {
278✔
89
            db: db.clone(),
278✔
90
            config: config.clone(),
278✔
91
        };
278✔
92
        let axum_router = axum::Router::new()
278✔
93
            // Temporary test endpoint to verify the proxy bridge works.
94
            // TODO: Remove once a real endpoint has been migrated.
95
            .route(
278✔
96
                "/internal/test/axum_ready",
278✔
97
                axum::routing::get(|| async { "axum OK" }),
278✔
98
            )
99
            .layer(DefaultBodyLimit::max(1024 * 1024))
278✔
100
            // Basic request tracing only for now; full telemetry (metrics,
101
            // OpenTelemetry, structured logging) will be added in Part 4.
102
            .layer(TraceLayer::new_for_http())
278✔
103
            .with_state(axum_state);
278✔
104
        let axum_listener = TcpListener::bind((Ipv6Addr::LOCALHOST, 0))
278✔
105
            .await
278✔
106
            .expect("failed to bind Axum listener on IPv6 loopback");
278✔
107
        let axum_addr = axum_listener
278✔
108
            .local_addr()
278✔
109
            .expect("failed to get Axum listener address");
278✔
110
        // TODO: Wire graceful shutdown into axum::serve(...).with_graceful_shutdown()
111
        // so that in-flight requests are drained when the Trillium server stops.
112
        tokio::spawn(async move {
278✔
113
            if let Err(e) = axum::serve(axum_listener, axum_router).await {
278✔
NEW
114
                log::error!("axum server error: {e}");
×
NEW
115
            }
×
NEW
116
        });
×
117

118
        let proxy = AxumProxy::new(axum_addr);
278✔
119

120
        Self {
278✔
121
            handler: Box::new((
278✔
122
                conn_id(),
278✔
123
                routes::health_check(&db),
278✔
124
                Forwarding::trust_always(),
278✔
125
                opentelemetry(),
278✔
126
                caching_headers(),
278✔
127
                logger(),
278✔
128
                #[cfg(assets)]
278✔
129
                instrument_handler(assets::static_assets(&config)),
278✔
130
                instrument_handler(api(&db, &config)),
278✔
131
                proxy,
278✔
132
                ErrorHandler,
278✔
133
            )),
278✔
134
            db,
278✔
135
            config,
278✔
136
            axum_addr,
278✔
137
        }
278✔
138
    }
278✔
139

140
    pub fn db(&self) -> &Db {
1,515✔
141
        &self.db
1,515✔
142
    }
1,515✔
143

144
    pub fn config(&self) -> &Config {
21✔
145
        &self.config
21✔
146
    }
21✔
147

148
    pub fn crypter(&self) -> &crate::Crypter {
231✔
149
        &self.config.crypter
231✔
150
    }
231✔
151

152
    #[allow(dead_code)] // Scaffolded for later migration parts.
NEW
153
    pub(crate) fn axum_addr(&self) -> SocketAddr {
×
NEW
154
        self.axum_addr
×
UNCOV
155
    }
×
156
}
157

158
impl AsRef<Db> for DivviupApi {
159
    fn as_ref(&self) -> &Db {
×
160
        &self.db
×
UNCOV
161
    }
×
162
}
163

164
#[derive(Handler, Debug, Clone)]
165
pub struct NamedHandler<H>(#[handler(except = name)] H, Cow<'static, str>);
166
impl<H: Handler> NamedHandler<H> {
167
    fn name(&self) -> Cow<'static, str> {
637✔
168
        self.1.clone()
637✔
169
    }
637✔
170

171
    pub fn new(name: impl Into<Cow<'static, str>>, handler: H) -> Self {
278✔
172
        Self(handler, name.into())
278✔
173
    }
278✔
174
}
175

176
fn api(db: &Db, config: &Config) -> impl Handler {
278✔
177
    NamedHandler::new(
278✔
178
        "api",
179
        (
278✔
180
            instrument_handler(compression()),
278✔
181
            #[cfg(feature = "integration-testing")]
278✔
182
            state(crate::User::for_integration_testing()),
278✔
183
            instrument_handler(cookies()),
278✔
184
            instrument_handler(
278✔
185
                sessions(
278✔
186
                    SessionStore::new(db.clone()),
278✔
187
                    &config.session_secrets.current,
278✔
188
                )
278✔
189
                .with_cookie_name("divviup.sid")
278✔
190
                .with_older_secrets(&config.session_secrets.older),
278✔
191
            ),
278✔
192
            state(config.client.clone()),
278✔
193
            state(config.crypter.clone()),
278✔
194
            state(config.feature_flags()),
278✔
195
            instrument_handler(cors_headers(config)),
278✔
196
            cache_control([Private, MustRevalidate]),
278✔
197
            db.clone(),
278✔
198
            instrument_handler(routes(config)),
278✔
199
        ),
278✔
200
    )
201
}
278✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc