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

divviup / divviup-api / 24378366361

14 Apr 2026 02:51AM UTC coverage: 57.463% (+0.3%) from 57.169%
24378366361

Pull #2197

github

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

76 of 95 new or added lines in 2 files covered. (80.0%)

2 existing lines in 1 file now uncovered.

4154 of 7229 relevant lines covered (57.46%)

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

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

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

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

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

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

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

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

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

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

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