Skip to content

Commit bed2ce0

Browse files
committed
fix: Bubble homeserver error back to user cleanly. Remove middleware
1 parent 815c7c0 commit bed2ce0

File tree

8 files changed

+23
-284
lines changed

8 files changed

+23
-284
lines changed

src/shared/homeserver_admin_api.rs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,4 @@ impl HomeserverAdminAPI {
5353
response.error_for_status()?;
5454
Ok(())
5555
}
56-
57-
/// Checks if the homeserver is responsive by making a GET request to the root URL
58-
/// This is NOT password protected so it won't validate the admin password.
59-
pub async fn health_check(&self) -> Result<(), reqwest::Error> {
60-
let url = self.base_url.join("/").expect("Failed to join URL path");
61-
let response = self.http_client.get(url).send().await?;
62-
response.error_for_status()?;
63-
Ok(())
64-
}
6556
}

src/shared/homeserver_health_middleware.rs

Lines changed: 0 additions & 135 deletions
This file was deleted.

src/shared/mod.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
mod homeserver_admin_api;
2-
mod homeserver_health_middleware;
32

43
pub use homeserver_admin_api::HomeserverAdminAPI;
5-
pub use homeserver_health_middleware::check_homeserver_health;

src/sms_verification/app_state.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ use crate::{
88
pub struct AppState {
99
pub db: SqlDb,
1010
pub sms_verification: SmsVerificationService,
11-
pub homeserver_admin_api: HomeserverAdminAPI,
1211
}
1312

1413
impl AppState {
@@ -28,7 +27,6 @@ impl AppState {
2827
Self {
2928
db,
3029
sms_verification,
31-
homeserver_admin_api,
3230
}
3331
}
3432
}

src/sms_verification/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ pub enum SmsVerificationError {
2424
#[error("External service rate limit exceeded")]
2525
RateLimited { retry_after: Option<u64> },
2626

27+
#[error("Homeserver temporarily unavailable, please retry")]
28+
HomeserverUnavailable,
29+
2730
#[error("HTTP request failed: {0}")]
2831
RequestFailed(#[from] reqwest::Error),
2932

src/sms_verification/http.rs

Lines changed: 4 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ use axum::{
22
Json, Router,
33
extract::State,
44
http::StatusCode,
5-
middleware,
65
response::{IntoResponse, Response},
76
routing::post,
87
};
@@ -22,22 +21,9 @@ pub async fn router(
2221
db: &crate::infrastructure::sql::SqlDb,
2322
) -> Result<Router, HttpServerError> {
2423
let state = AppState::new(config, db.clone());
25-
let homeserver_api = state.homeserver_admin_api.clone();
26-
27-
// NOTE: SMS verification routes REQUIRE homeserver health check middleware.
28-
// Unlike LN verification, SMS cannot use database transactions for atomicity because:
29-
// 1. Prelude API destructively marks verification as complete when check_code() succeeds
30-
// 2. If generate_signup_token() fails after that, we can't rollback Prelude's state
31-
// 3. This would create an inconsistent state: Prelude says "verified", our DB says "failed"
32-
//
33-
// The middleware mitigates this by failing fast (503) if homeserver is unreachable.
3424
Ok(Router::new()
3525
.route("/send_code", post(send_code_handler))
3626
.route("/validate_code", post(validate_code_handler))
37-
.route_layer(middleware::from_fn(move |req, next| {
38-
let api = homeserver_api.clone();
39-
crate::shared::check_homeserver_health(api, req, next)
40-
}))
4127
.with_state(state))
4228
}
4329

@@ -95,6 +81,10 @@ impl IntoResponse for SmsVerificationError {
9581
}
9682
return response;
9783
}
84+
SmsVerificationError::HomeserverUnavailable => {
85+
tracing::error!("Homeserver unavailable during SMS verification");
86+
StatusCode::INTERNAL_SERVER_ERROR
87+
}
9888
SmsVerificationError::RequestFailed(ref err) => {
9989
tracing::error!(error = %err, "Failed to communicate with SMS provider");
10090
StatusCode::INTERNAL_SERVER_ERROR
@@ -309,124 +299,4 @@ mod tests {
309299

310300
// Wiremock verifies all expected calls were made
311301
}
312-
313-
/// Integration tests that verify the homeserver health middleware actually works
314-
/// when integrated with the full router (not router_with_db which bypasses middleware)
315-
mod middleware_integration {
316-
use super::*;
317-
use crate::e2e::WiremockServers;
318-
use wiremock::{
319-
Mock, ResponseTemplate,
320-
matchers::{method, path},
321-
};
322-
323-
/// Helper to create a router with the production path that includes middleware
324-
async fn create_router_with_middleware(
325-
pool: PgPool,
326-
servers: &WiremockServers,
327-
) -> (TestServer, PgPool) {
328-
use crate::EnvConfig;
329-
use crate::infrastructure::sql::SqlDb;
330-
331-
let config = EnvConfig::for_test(
332-
servers.prelude_server.uri().parse().unwrap(),
333-
servers.homeserver_server.uri().parse().unwrap(),
334-
);
335-
336-
let db = SqlDb::test(pool.clone()).await;
337-
338-
// Use the production router() function which includes middleware
339-
let sms_verification_router =
340-
router(&config, &db).await.expect("Failed to create router");
341-
342-
let router = Router::new().nest("/sms_verification", sms_verification_router);
343-
let app = router.into_make_service_with_connect_info::<SocketAddr>();
344-
let server = TestServer::new(app).expect("Failed to create test server");
345-
346-
(server, pool)
347-
}
348-
349-
#[sqlx::test]
350-
async fn middleware_blocks_send_code_when_homeserver_is_down(pool: PgPool) {
351-
let servers = WiremockServers::start().await;
352-
353-
// Setup homeserver to be unresponsive (return 500)
354-
Mock::given(method("GET"))
355-
.and(path("/"))
356-
.respond_with(ResponseTemplate::new(500))
357-
.expect(1)
358-
.mount(&servers.homeserver_server)
359-
.await;
360-
361-
let (server, _pool) = create_router_with_middleware(pool, &servers).await;
362-
363-
// Attempt to send code
364-
let response = server
365-
.post("/sms_verification/send_code")
366-
.json(&serde_json::json!({ "phoneNumber": "+30123456789" }))
367-
.await;
368-
369-
// Should return 503 due to homeserver being down
370-
response.assert_status(StatusCode::SERVICE_UNAVAILABLE);
371-
assert_eq!(
372-
response.text(),
373-
"Homeserver temporarily unavailable, please retry"
374-
);
375-
}
376-
377-
#[sqlx::test]
378-
async fn middleware_blocks_validate_code_when_homeserver_is_down(pool: PgPool) {
379-
let servers = WiremockServers::start().await;
380-
381-
// Setup homeserver to return 500
382-
Mock::given(method("GET"))
383-
.and(path("/"))
384-
.respond_with(ResponseTemplate::new(500))
385-
.expect(1)
386-
.mount(&servers.homeserver_server)
387-
.await;
388-
389-
let (server, _pool) = create_router_with_middleware(pool, &servers).await;
390-
391-
// Attempt to validate code when homeserver is down
392-
// Should be blocked by middleware before reaching the handler
393-
let response = server
394-
.post("/sms_verification/validate_code")
395-
.json(&serde_json::json!({
396-
"phoneNumber": "+30123456789",
397-
"code": "123456"
398-
}))
399-
.await;
400-
401-
// Should return 503 due to homeserver being down
402-
// The middleware blocks the request before it reaches the handler
403-
response.assert_status(StatusCode::SERVICE_UNAVAILABLE);
404-
assert_eq!(
405-
response.text(),
406-
"Homeserver temporarily unavailable, please retry"
407-
);
408-
}
409-
410-
#[sqlx::test]
411-
async fn middleware_blocks_when_homeserver_is_unreachable(pool: PgPool) {
412-
let servers = WiremockServers::start().await;
413-
414-
// Don't mount any mock - this simulates connection refused
415-
// The middleware will try to connect and fail
416-
417-
let (server, _pool) = create_router_with_middleware(pool, &servers).await;
418-
419-
// Attempt to send code - should be blocked immediately by middleware
420-
let response = server
421-
.post("/sms_verification/send_code")
422-
.json(&serde_json::json!({ "phoneNumber": "+30123456789" }))
423-
.await;
424-
425-
response.assert_status(StatusCode::SERVICE_UNAVAILABLE);
426-
assert_eq!(
427-
response.text(),
428-
"Homeserver temporarily unavailable, please retry"
429-
);
430-
}
431-
}
432302
}

src/sms_verification/service.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ impl SmsVerificationService {
159159
PreludeCheckCodeResponse::Success { id, .. } => {
160160
let code = match self.homeserver_admin_api.generate_signup_token().await {
161161
Ok(code) => code,
162-
Err(e) => {
162+
Err(_e) => {
163163
if let Err(e) = SmsVerificationRepository::mark_failed(
164164
&mut executor,
165165
&id,
@@ -169,7 +169,7 @@ impl SmsVerificationService {
169169
{
170170
tracing::error!("{}", e);
171171
}
172-
return Err(e.into());
172+
return Err(SmsVerificationError::HomeserverUnavailable);
173173
}
174174
};
175175

src/sms_verification/tests.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,20 @@ async fn test_service_success_but_homeserver_fails_marks_failed(pool: PgPool) {
794794
record.failure_reason,
795795
Some("homeserver_signup_token_generation_failed".to_string())
796796
);
797+
798+
// Verify that failed verification does NOT count towards quota
799+
let phone_hash = test_phone_hasher().hash_phone_number(phone.as_str());
800+
let failed_count = SmsVerificationRepository::count_verified_sessions_in_last_days(
801+
&mut executor,
802+
&phone_hash,
803+
7,
804+
)
805+
.await
806+
.unwrap();
807+
assert_eq!(
808+
failed_count, 0,
809+
"Failed verification should NOT count towards weekly quota"
810+
);
797811
}
798812

799813
// This circumstance happens if validate_code() is called before send_code() - Prelude has no prelude_id for the verification session yet so it returns a dummy value which doesnt match to anything in our db.

0 commit comments

Comments
 (0)