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

divviup / divviup-api / 24377944464

14 Apr 2026 02:36AM UTC coverage: 57.434% (+0.3%) from 57.169%
24377944464

Pull #2197

github

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

71 of 90 new or added lines in 2 files covered. (78.89%)

2 existing lines in 1 file now uncovered.

4149 of 7224 relevant lines covered (57.43%)

60.66 hits per line

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

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

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

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

44
use self::opentelemetry::opentelemetry;
45

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

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

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

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

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

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

112
        let proxy = AxumProxy::new(axum_addr);
277✔
113

114
        Self {
277✔
115
            handler: Box::new((
277✔
116
                conn_id(),
277✔
117
                routes::health_check(&db),
277✔
118
                Forwarding::trust_always(),
277✔
119
                opentelemetry(),
277✔
120
                caching_headers(),
277✔
121
                logger(),
277✔
122
                #[cfg(assets)]
277✔
123
                instrument_handler(assets::static_assets(&config)),
277✔
124
                instrument_handler(api(&db, &config)),
277✔
125
                proxy,
277✔
126
                ErrorHandler,
277✔
127
            )),
277✔
128
            db,
277✔
129
            config,
277✔
130
            axum_addr,
277✔
131
        }
277✔
132
    }
277✔
133

134
    pub fn db(&self) -> &Db {
1,514✔
135
        &self.db
1,514✔
136
    }
1,514✔
137

138
    pub fn config(&self) -> &Config {
21✔
139
        &self.config
21✔
140
    }
21✔
141

142
    pub fn crypter(&self) -> &crate::Crypter {
231✔
143
        &self.config.crypter
231✔
144
    }
231✔
145

NEW
146
    pub fn axum_addr(&self) -> SocketAddr {
×
NEW
147
        self.axum_addr
×
NEW
148
    }
×
149
}
150

151
impl AsRef<Db> for DivviupApi {
152
    fn as_ref(&self) -> &Db {
×
153
        &self.db
×
154
    }
×
155
}
156

157
#[derive(Handler, Debug, Clone)]
158
pub struct NamedHandler<H>(#[handler(except = name)] H, Cow<'static, str>);
159
impl<H: Handler> NamedHandler<H> {
160
    fn name(&self) -> Cow<'static, str> {
636✔
161
        self.1.clone()
636✔
162
    }
636✔
163

164
    pub fn new(name: impl Into<Cow<'static, str>>, handler: H) -> Self {
277✔
165
        Self(handler, name.into())
277✔
166
    }
277✔
167
}
168

169
fn api(db: &Db, config: &Config) -> impl Handler {
277✔
170
    NamedHandler::new(
277✔
171
        "api",
172
        (
277✔
173
            instrument_handler(compression()),
277✔
174
            #[cfg(feature = "integration-testing")]
277✔
175
            state(crate::User::for_integration_testing()),
277✔
176
            instrument_handler(cookies()),
277✔
177
            instrument_handler(
277✔
178
                sessions(
277✔
179
                    SessionStore::new(db.clone()),
277✔
180
                    &config.session_secrets.current,
277✔
181
                )
277✔
182
                .with_cookie_name("divviup.sid")
277✔
183
                .with_older_secrets(&config.session_secrets.older),
277✔
184
            ),
277✔
185
            state(config.client.clone()),
277✔
186
            state(config.crypter.clone()),
277✔
187
            state(config.feature_flags()),
277✔
188
            instrument_handler(cors_headers(config)),
277✔
189
            cache_control([Private, MustRevalidate]),
277✔
190
            db.clone(),
277✔
191
            instrument_handler(routes(config)),
277✔
192
        ),
277✔
193
    )
194
}
277✔
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