diff --git a/crates/edda-serve/src/api/analytics.rs b/crates/edda-serve/src/api/analytics.rs index 10e2f62..00b8d8a 100644 --- a/crates/edda-serve/src/api/analytics.rs +++ b/crates/edda-serve/src/api/analytics.rs @@ -85,7 +85,9 @@ async fn get_recap( Query(params): Query, ) -> Result, AppError> { if state.chronicle.is_none() { - return Err(anyhow::anyhow!("chronicle feature not enabled").into()); + return Err(AppError::NotImplemented( + "chronicle feature not enabled".into(), + )); } let anchor = if let Some(ref project) = params.project { @@ -149,7 +151,9 @@ async fn get_recap_cached( Query(params): Query, ) -> Result, AppError> { if state.chronicle.is_none() { - return Err(anyhow::anyhow!("chronicle feature not enabled").into()); + return Err(AppError::NotImplemented( + "chronicle feature not enabled".into(), + )); } let anchor = if let Some(ref project) = params.project { @@ -221,7 +225,9 @@ async fn get_overview( State(state): State>, ) -> Result, AppError> { if state.chronicle.is_none() { - return Err(anyhow::anyhow!("chronicle feature not enabled").into()); + return Err(AppError::NotImplemented( + "chronicle feature not enabled".into(), + )); } let projects = list_projects(); @@ -293,7 +299,9 @@ async fn get_projects( State(state): State>, ) -> Result, AppError> { if state.chronicle.is_none() { - return Err(anyhow::anyhow!("chronicle feature not enabled").into()); + return Err(AppError::NotImplemented( + "chronicle feature not enabled".into(), + )); } let projects = list_projects(); diff --git a/crates/edda-serve/src/api/auth.rs b/crates/edda-serve/src/api/auth.rs index 8e7849f..2478776 100644 --- a/crates/edda-serve/src/api/auth.rs +++ b/crates/edda-serve/src/api/auth.rs @@ -1,7 +1,6 @@ use std::sync::Arc; use std::time::Duration; -use anyhow::Context; use axum::extract::rejection::JsonRejection; use axum::extract::{Query, State}; use axum::http::HeaderMap; @@ -126,7 +125,7 @@ async fn complete_pairing( let event_id = format!("evt_{}", ulid::Ulid::new()); // Write device_pair event to ledger - let ledger = state.open_ledger().context("GET /pair")?; + let ledger = state.open_ledger()?; let branch = ledger.head_branch()?; let payload = serde_json::json!({ @@ -183,7 +182,7 @@ struct DeviceInfo { async fn list_paired_devices( State(state): State>, ) -> Result>, AppError> { - let ledger = state.open_ledger().context("GET /api/pair/list")?; + let ledger = state.open_ledger()?; let tokens = ledger.list_device_tokens()?; let devices: Vec = tokens @@ -215,7 +214,7 @@ async fn revoke_device( ) -> Result, AppError> { let Json(req) = body.map_err(|e| AppError::Validation(e.to_string()))?; - let ledger = state.open_ledger().context("POST /api/pair/revoke")?; + let ledger = state.open_ledger()?; // Check the token exists *before* writing the ledger event let existing = ledger.list_device_tokens()?; @@ -273,7 +272,7 @@ async fn revoke_all_devices( State(state): State>, ) -> Result, AppError> { let event_id = format!("evt_{}", ulid::Ulid::new()); - let ledger = state.open_ledger().context("POST /api/pair/revoke-all")?; + let ledger = state.open_ledger()?; let branch = ledger.head_branch()?; let now = time::OffsetDateTime::now_utc(); diff --git a/crates/edda-serve/src/api/briefs.rs b/crates/edda-serve/src/api/briefs.rs index 47ca9f0..a6c8607 100644 --- a/crates/edda-serve/src/api/briefs.rs +++ b/crates/edda-serve/src/api/briefs.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use anyhow::Context; use axum::extract::{Path as AxumPath, Query, State}; use axum::response::IntoResponse; use axum::routing::get; @@ -35,7 +34,7 @@ struct ActorsListResponse { async fn get_actors( State(state): State>, ) -> Result, AppError> { - let ledger = state.open_ledger().context("GET /api/actors")?; + let ledger = state.open_ledger()?; let cfg = policy::load_actors_from_dir(&ledger.paths.edda_dir)?; let actors = cfg .actors @@ -58,7 +57,7 @@ async fn get_actor( State(state): State>, AxumPath(name): AxumPath, ) -> Result, AppError> { - let ledger = state.open_ledger().context("GET /api/actors/:name")?; + let ledger = state.open_ledger()?; let cfg = policy::load_actors_from_dir(&ledger.paths.edda_dir)?; match cfg.actors.get(&name) { Some(def) => Ok(Json(ActorResponse { @@ -90,7 +89,7 @@ async fn get_briefs( State(state): State>, Query(params): Query, ) -> Result, AppError> { - let ledger = state.open_ledger().context("GET /api/briefs")?; + let ledger = state.open_ledger()?; let briefs = ledger.list_task_briefs(params.status.as_deref(), params.intent.as_deref())?; let items: Vec = briefs @@ -125,7 +124,7 @@ async fn get_brief( State(state): State>, AxumPath(task_id): AxumPath, ) -> Result, AppError> { - let ledger = state.open_ledger().context("GET /api/briefs/:task_id")?; + let ledger = state.open_ledger()?; let brief = ledger .get_task_brief(&task_id)? .ok_or_else(|| AppError::NotFound(format!("task brief not found: {task_id}")))?; diff --git a/crates/edda-serve/src/api/drafts.rs b/crates/edda-serve/src/api/drafts.rs index 05e8137..41a3259 100644 --- a/crates/edda-serve/src/api/drafts.rs +++ b/crates/edda-serve/src/api/drafts.rs @@ -1,7 +1,6 @@ use std::path::Path; use std::sync::Arc; -use anyhow::Context; use axum::extract::rejection::JsonRejection; use axum::extract::{Path as AxumPath, State}; use axum::http::{HeaderMap, StatusCode}; @@ -82,7 +81,7 @@ struct MinimalStage { } async fn get_drafts(State(state): State>) -> Result, AppError> { - let ledger = state.open_ledger().context("GET /api/drafts")?; + let ledger = state.open_ledger()?; let drafts_dir = &ledger.paths.drafts_dir; if !drafts_dir.exists() { @@ -280,7 +279,7 @@ async fn handle_draft_action( action: &str, body: &ApproveRequest, ) -> Result { - let ledger = state.open_ledger().context("POST /api/drafts/:id/action")?; + let ledger = state.open_ledger()?; let _lock = WorkspaceLock::acquire(&ledger.paths)?; // Read the draft diff --git a/crates/edda-serve/src/api/events.rs b/crates/edda-serve/src/api/events.rs index 6fb1d22..ffff4bf 100644 --- a/crates/edda-serve/src/api/events.rs +++ b/crates/edda-serve/src/api/events.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use anyhow::Context; use axum::extract::rejection::JsonRejection; use axum::extract::{Path as AxumPath, Query, State}; use axum::http::StatusCode; @@ -40,7 +39,7 @@ struct LastCommit { } async fn get_status(State(state): State>) -> Result, AppError> { - let ledger = state.open_ledger().context("GET /api/status")?; + let ledger = state.open_ledger()?; let head = ledger.head_branch()?; let snap = rebuild_branch(&ledger, &head)?; @@ -73,7 +72,7 @@ async fn get_context( State(state): State>, Query(params): Query, ) -> Result, AppError> { - let ledger = state.open_ledger().context("GET /api/context")?; + let ledger = state.open_ledger()?; let head = ledger.head_branch()?; let depth = params.depth.unwrap_or(5); let text = render_context(&ledger, &head, DeriveOptions { depth })?; @@ -110,7 +109,7 @@ async fn get_decisions( crate::helpers::validate_iso8601(before).map_err(AppError::Validation)?; } - let ledger = state.open_ledger().context("GET /api/decisions")?; + let ledger = state.open_ledger()?; let q = params .q .as_deref() @@ -195,7 +194,7 @@ async fn post_decisions_batch( )); } - let ledger = state.open_ledger().context("POST /api/decisions/batch")?; + let ledger = state.open_ledger()?; let mut results = Vec::with_capacity(body.queries.len()); for (i, sub) in body.queries.iter().enumerate() { @@ -264,9 +263,7 @@ async fn get_decision_outcomes( State(state): State>, AxumPath(event_id): AxumPath, ) -> Result { - let ledger = state - .open_ledger() - .context("GET /api/decisions/:id/outcomes")?; + let ledger = state.open_ledger()?; let outcomes = ledger.decision_outcomes(&event_id)?; match outcomes { @@ -321,9 +318,7 @@ async fn get_decision_chain( Query(params): Query, ) -> Result, AppError> { let depth = params.depth.unwrap_or(3).min(10); - let ledger = state - .open_ledger() - .context("GET /api/decisions/:id/chain")?; + let ledger = state.open_ledger()?; let (root, chain) = ledger .causal_chain(&event_id, depth)? @@ -397,7 +392,7 @@ async fn get_log( State(state): State>, Query(params): Query, ) -> Result, AppError> { - let ledger = state.open_ledger().context("GET /api/log")?; + let ledger = state.open_ledger()?; let head = ledger.head_branch()?; let limit = params.limit.unwrap_or(50); @@ -463,7 +458,7 @@ async fn post_note( ) -> Result { let Json(body) = body.map_err(|e| AppError::Validation(e.body_text()))?; - let ledger = state.open_ledger().context("POST /api/note")?; + let ledger = state.open_ledger()?; let _lock = WorkspaceLock::acquire(&ledger.paths)?; let branch = ledger.head_branch()?; @@ -511,7 +506,7 @@ async fn post_decide( let key = key.trim(); let value = value.trim(); - let ledger = state.open_ledger().context("POST /api/decide")?; + let ledger = state.open_ledger()?; let _lock = WorkspaceLock::acquire(&ledger.paths)?; let branch = ledger.head_branch()?; @@ -633,7 +628,7 @@ async fn post_karvi_event( "decision_ref": body.decision_ref, }); - let ledger = state.open_ledger().context("POST /api/events/karvi")?; + let ledger = state.open_ledger()?; let _lock = WorkspaceLock::acquire(&ledger.paths)?; let branch = ledger.head_branch()?; let parent_hash = ledger.last_event_hash()?; diff --git a/crates/edda-serve/src/api/metrics.rs b/crates/edda-serve/src/api/metrics.rs index 24ccaa1..63c4887 100644 --- a/crates/edda-serve/src/api/metrics.rs +++ b/crates/edda-serve/src/api/metrics.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use anyhow::Context; use axum::extract::rejection::JsonRejection; use axum::extract::{Path as AxumPath, Query, State}; use axum::routing::{get, post}; @@ -34,7 +33,7 @@ async fn get_quality_metrics( after: params.after, before: params.before, }; - let ledger = state.open_ledger().context("GET /api/metrics/quality")?; + let ledger = state.open_ledger()?; let events = ledger.iter_events_by_type("execution_event")?; let report = model_quality_from_events(&events, &range); Ok(Json(report)) @@ -63,9 +62,7 @@ async fn get_controls_suggestions( after: params.after, before: params.before, }; - let ledger = state - .open_ledger() - .context("GET /api/controls/suggestions")?; + let ledger = state.open_ledger()?; let events = ledger.iter_events_by_type("execution_event")?; let report = model_quality_from_events(&events, &range); @@ -170,7 +167,9 @@ async fn get_metrics_overview( Query(params): Query, ) -> Result, AppError> { if state.chronicle.is_none() { - return Err(anyhow::anyhow!("chronicle feature not enabled").into()); + return Err(AppError::NotImplemented( + "chronicle feature not enabled".into(), + )); } let all_projects = list_projects(); @@ -268,7 +267,9 @@ async fn get_metrics_trends( Query(params): Query, ) -> Result, AppError> { if state.chronicle.is_none() { - return Err(anyhow::anyhow!("chronicle feature not enabled").into()); + return Err(AppError::NotImplemented( + "chronicle feature not enabled".into(), + )); } let all_projects = list_projects(); diff --git a/crates/edda-serve/src/api/policy.rs b/crates/edda-serve/src/api/policy.rs index 21c236f..8d5b409 100644 --- a/crates/edda-serve/src/api/policy.rs +++ b/crates/edda-serve/src/api/policy.rs @@ -1,13 +1,11 @@ use std::sync::Arc; -use anyhow::Context; use axum::extract::{Path as AxumPath, Query, State}; use axum::routing::{get, post}; use axum::{Json, Router}; use serde::{Deserialize, Serialize}; use edda_core::policy; -use edda_ledger::Ledger; use crate::error::AppError; use crate::state::AppState; @@ -208,7 +206,7 @@ async fn post_approval_check( // Build ReviewBundle from request or from ledger let bundle = if let Some(bundle_id) = &body.bundle_id { - let ledger = Ledger::open(&state.repo_root).context("POST /api/approval/check")?; + let ledger = state.open_ledger()?; let Some(row) = ledger.get_bundle(bundle_id)? else { return Err(AppError::NotFound(format!( "Bundle '{}' not found", diff --git a/crates/edda-serve/src/api/snapshots.rs b/crates/edda-serve/src/api/snapshots.rs index b1dbbe5..064af6d 100644 --- a/crates/edda-serve/src/api/snapshots.rs +++ b/crates/edda-serve/src/api/snapshots.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use anyhow::Context; use axum::extract::rejection::JsonRejection; use axum::extract::{Path as AxumPath, Query, State}; use axum::http::StatusCode; @@ -63,7 +62,7 @@ async fn post_snapshot( )); } - let ledger = state.open_ledger().context("POST /api/snapshot")?; + let ledger = state.open_ledger()?; let _lock = WorkspaceLock::acquire(&ledger.paths)?; let branch = ledger.head_branch()?; @@ -165,7 +164,7 @@ async fn get_snapshots( State(state): State>, Query(query): Query, ) -> Result { - let ledger = state.open_ledger().context("GET /api/snapshots")?; + let ledger = state.open_ledger()?; let rows = ledger.query_snapshots( query.village_id.as_deref(), query.engine_version.as_deref(), @@ -187,7 +186,7 @@ async fn get_snapshots_by_hash( State(state): State>, AxumPath(context_hash): AxumPath, ) -> Result { - let ledger = state.open_ledger().context("GET /api/snapshots/:hash")?; + let ledger = state.open_ledger()?; let rows = ledger.snapshots_by_context_hash(&context_hash)?; if rows.is_empty() { @@ -227,7 +226,7 @@ async fn get_village_stats( crate::helpers::validate_iso8601(before).map_err(AppError::Validation)?; } - let ledger = state.open_ledger().context("GET /api/villages/:id/stats")?; + let ledger = state.open_ledger()?; let stats = ledger.village_stats( &village_id, params.after.as_deref(), @@ -268,7 +267,7 @@ async fn get_patterns( .format(&time::format_description::well_known::Rfc3339) .unwrap_or_default(); - let ledger = state.open_ledger().context("GET /api/patterns")?; + let ledger = state.open_ledger()?; let patterns = ledger.detect_village_patterns(village_id, &after_str, min_occurrences)?; let total = patterns.len(); @@ -288,7 +287,7 @@ fn reconstruct_snapshot( ) -> Result { let event = ledger .get_event(&row.event_id)? - .ok_or_else(|| AppError::Internal(anyhow::anyhow!("event {} not found", row.event_id)))?; + .ok_or_else(|| AppError::NotFound(format!("event {} not found", row.event_id)))?; let payload = &event.payload; diff --git a/crates/edda-serve/src/api/stream.rs b/crates/edda-serve/src/api/stream.rs index a00e979..e414b96 100644 --- a/crates/edda-serve/src/api/stream.rs +++ b/crates/edda-serve/src/api/stream.rs @@ -2,7 +2,6 @@ use std::convert::Infallible; use std::sync::Arc; use std::time::Duration; -use anyhow::Context; use axum::extract::{Query, State}; use axum::http::HeaderMap; use axum::response::sse::{Event as SseEvent, KeepAlive}; @@ -71,7 +70,7 @@ async fn get_event_stream( // Resolve the initial cursor (rowid) from `since` event_id. let mut cursor: i64 = if let Some(ref event_id) = since { - let ledger = state.open_ledger().context("GET /api/events/stream")?; + let ledger = state.open_ledger()?; ledger.rowid_for_event_id(event_id)?.unwrap_or(0) } else { 0 diff --git a/crates/edda-serve/src/api/telemetry.rs b/crates/edda-serve/src/api/telemetry.rs index 60c52f1..188e85c 100644 --- a/crates/edda-serve/src/api/telemetry.rs +++ b/crates/edda-serve/src/api/telemetry.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use anyhow::Context; use axum::extract::rejection::JsonRejection; use axum::extract::{Query, State}; use axum::http::StatusCode; @@ -80,7 +79,7 @@ async fn post_telemetry( "metadata": body.metadata, }); - let ledger = state.open_ledger().context("POST /api/telemetry")?; + let ledger = state.open_ledger()?; let _lock = WorkspaceLock::acquire(&ledger.paths)?; let branch = ledger.head_branch()?; let parent_hash = ledger.last_event_hash()?; @@ -131,7 +130,7 @@ async fn get_telemetry( State(state): State>, Query(q): Query, ) -> Result { - let ledger = state.open_ledger().context("GET /api/telemetry")?; + let ledger = state.open_ledger()?; let branch = ledger.head_branch()?; let limit = q.limit.unwrap_or(100); @@ -182,7 +181,7 @@ async fn get_telemetry_stats( State(state): State>, Query(q): Query, ) -> Result { - let ledger = state.open_ledger().context("GET /api/telemetry/stats")?; + let ledger = state.open_ledger()?; let branch = ledger.head_branch()?; let days = q.days.unwrap_or(7); diff --git a/crates/edda-serve/src/error.rs b/crates/edda-serve/src/error.rs index 6401664..6d16e79 100644 --- a/crates/edda-serve/src/error.rs +++ b/crates/edda-serve/src/error.rs @@ -18,6 +18,12 @@ pub(crate) enum AppError { #[error("{0}")] Unauthorized(String), + #[error("{0}")] + ServiceUnavailable(String), + + #[error("{0}")] + NotImplemented(String), + #[error("{0}")] Internal(#[from] anyhow::Error), } @@ -53,12 +59,43 @@ impl IntoResponse for AppError { AppError::NotFound(_) => (StatusCode::NOT_FOUND, "NOT_FOUND"), AppError::Conflict(_) => (StatusCode::CONFLICT, "CONFLICT"), AppError::Unauthorized(_) => (StatusCode::UNAUTHORIZED, "UNAUTHORIZED"), + AppError::ServiceUnavailable(_) => { + (StatusCode::SERVICE_UNAVAILABLE, "SERVICE_UNAVAILABLE") + } + AppError::NotImplemented(_) => (StatusCode::NOT_IMPLEMENTED, "NOT_IMPLEMENTED"), AppError::Internal(_) => (StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR"), }; let body = serde_json::json!({ "error": self.to_string(), "code": code, }); + if matches!(&self, AppError::ServiceUnavailable(_)) { + let mut resp = (status, Json(body)).into_response(); + resp.headers_mut() + .insert("retry-after", "1".parse().expect("valid header value")); + return resp; + } (status, Json(body)).into_response() } } + +/// Classify a ledger `open()` error into the appropriate `AppError` variant. +/// +/// - "not an edda workspace" → `NotFound` (project not initialized) +/// - "database is locked" → `ServiceUnavailable` (transient SQLite busy) +/// - Everything else → `Internal` +pub(crate) fn classify_open_error(err: anyhow::Error) -> AppError { + let msg = format!("{err:#}"); + + if msg.contains("not an edda workspace") { + return AppError::NotFound(err.to_string()); + } + + if msg.contains("database is locked") { + return AppError::ServiceUnavailable( + "database is temporarily unavailable, please retry".into(), + ); + } + + AppError::Internal(err) +} diff --git a/crates/edda-serve/src/lib.rs b/crates/edda-serve/src/lib.rs index 2730e83..10e67ad 100644 --- a/crates/edda-serve/src/lib.rs +++ b/crates/edda-serve/src/lib.rs @@ -18,8 +18,7 @@ use tower_http::cors::{AllowOrigin, CorsLayer}; #[cfg(test)] use crate::error::AppError; -#[cfg(test)] -use anyhow::Context; + #[cfg(test)] use axum::extract::rejection::JsonRejection; #[cfg(test)] @@ -217,7 +216,7 @@ async fn post_sync( dry_run: false, }); - let ledger = state.open_ledger().context("POST /api/sync")?; + let ledger = state.open_ledger()?; let sources = if let Some(name) = &body.from { sources_from_name(name) @@ -4654,4 +4653,123 @@ actors: .unwrap(); assert_eq!(resp3.status(), StatusCode::CONFLICT); } + + // ── Error differentiation tests (GH-379) ── + + /// Build a router without chronicle context (for testing 501 responses). + fn router_no_chronicle(repo_root: &Path) -> Router { + let state = Arc::new(AppState { + repo_root: repo_root.to_path_buf(), + chronicle: None, + pending_pairings: Mutex::new(HashMap::new()), + }); + api::events::routes() + .merge(api::drafts::routes()) + .merge(api::telemetry::routes()) + .merge(api::snapshots::routes()) + .merge(api::analytics::routes()) + .merge(api::metrics::routes()) + .merge(api::dashboard::routes()) + .merge(api::policy::routes()) + .merge(api::briefs::routes()) + .merge(api::stream::routes()) + .merge(api::ingestion::routes()) + .merge(api::auth::routes()) + .with_state(state) + } + + #[test] + fn classify_open_error_not_edda_workspace() { + use crate::error::classify_open_error; + let err = anyhow::anyhow!("not an edda workspace (run `edda init` first)"); + match classify_open_error(err) { + crate::error::AppError::NotFound(_) => {} + other => panic!("expected NotFound, got {other:?}"), + } + } + + #[test] + fn classify_open_error_database_locked() { + use crate::error::classify_open_error; + let err = anyhow::anyhow!("database is locked"); + match classify_open_error(err) { + crate::error::AppError::ServiceUnavailable(_) => {} + other => panic!("expected ServiceUnavailable, got {other:?}"), + } + } + + #[test] + fn classify_open_error_unknown_becomes_internal() { + use crate::error::classify_open_error; + let err = anyhow::anyhow!("some random error"); + match classify_open_error(err) { + crate::error::AppError::Internal(_) => {} + other => panic!("expected Internal, got {other:?}"), + } + } + + #[tokio::test] + async fn status_returns_404_for_non_edda_workspace() { + let tmp = tempfile::tempdir().unwrap(); + // Do NOT call setup_workspace — leave it as a bare directory + let app = router_no_chronicle(tmp.path()); + + let resp = app + .oneshot( + Request::builder() + .uri("/api/status") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + let body = axum::body::to_bytes(resp.into_body(), usize::MAX) + .await + .unwrap(); + let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); + assert_eq!(json["code"], "NOT_FOUND"); + } + + #[tokio::test] + async fn recap_returns_501_without_chronicle() { + let tmp = tempfile::tempdir().unwrap(); + setup_workspace(tmp.path()); + let app = router_no_chronicle(tmp.path()); + + let resp = app + .oneshot( + Request::builder() + .uri("/api/recap") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(resp.status(), StatusCode::NOT_IMPLEMENTED); + let body = axum::body::to_bytes(resp.into_body(), usize::MAX) + .await + .unwrap(); + let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); + assert_eq!(json["code"], "NOT_IMPLEMENTED"); + } + + #[tokio::test] + async fn service_unavailable_includes_retry_after_header() { + use crate::error::AppError; + use axum::response::IntoResponse; + + let err = AppError::ServiceUnavailable("database is temporarily unavailable".into()); + let resp = err.into_response(); + + assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE); + assert_eq!( + resp.headers() + .get("retry-after") + .and_then(|v| v.to_str().ok()), + Some("1") + ); + } } diff --git a/crates/edda-serve/src/middleware.rs b/crates/edda-serve/src/middleware.rs index 4bd75fe..1c4e461 100644 --- a/crates/edda-serve/src/middleware.rs +++ b/crates/edda-serve/src/middleware.rs @@ -1,7 +1,6 @@ use std::net::SocketAddr; use std::sync::Arc; -use anyhow::Context; use axum::extract::{ConnectInfo, State}; use axum::http::Request; use axum::middleware::Next; @@ -65,7 +64,7 @@ pub(crate) async fn auth_middleware( }; let token_hash = hash_token(raw_token); - let ledger = state.open_ledger().context("auth_middleware")?; + let ledger = state.open_ledger()?; let device = ledger.validate_device_token(&token_hash)?; match device { diff --git a/crates/edda-serve/src/state.rs b/crates/edda-serve/src/state.rs index 714042a..23de11c 100644 --- a/crates/edda-serve/src/state.rs +++ b/crates/edda-serve/src/state.rs @@ -29,7 +29,7 @@ pub(crate) struct ChronicleContext { } impl AppState { - pub(crate) fn open_ledger(&self) -> anyhow::Result { - Ledger::open(&self.repo_root) + pub(crate) fn open_ledger(&self) -> Result { + Ledger::open(&self.repo_root).map_err(crate::error::classify_open_error) } }