From 2bce469ed26e8e081a7909c327434ba58633473a Mon Sep 17 00:00:00 2001 From: Anders Olsson Date: Thu, 4 Jun 2026 17:17:35 +0200 Subject: [PATCH] fix(api): 404 when adding a term to a missing vocabulary (#22); log public 500s (#18) Co-Authored-By: Claude Sonnet 4.6 --- crates/api/src/admin_vocab.rs | 12 +++++++++--- crates/api/src/public.rs | 15 ++++++++++++--- crates/api/tests/admin_catalog.rs | 27 +++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/crates/api/src/admin_vocab.rs b/crates/api/src/admin_vocab.rs index e42d446..18a2e71 100644 --- a/crates/api/src/admin_vocab.rs +++ b/crates/api/src/admin_vocab.rs @@ -185,9 +185,15 @@ pub(crate) async fn add_term( .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let term_id = db::vocab::add_term(&mut tx, &new) - .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let term_id = db::vocab::add_term(&mut tx, &new).await.map_err(|err| { + // A well-formed id for a missing vocabulary hits the FK constraint (23503). + if err.as_database_error().and_then(|e| e.code()).as_deref() == Some("23503") { + StatusCode::NOT_FOUND + } else { + tracing::error!(?err, "adding term"); + StatusCode::INTERNAL_SERVER_ERROR + } + })?; tx.commit() .await diff --git a/crates/api/src/public.rs b/crates/api/src/public.rs index b36f2c7..15a5d05 100644 --- a/crates/api/src/public.rs +++ b/crates/api/src/public.rs @@ -71,11 +71,17 @@ pub(crate) async fn list_objects( // public read surface. let objects = db::catalog::list_public_objects(state.db.pool(), limit, offset) .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + .map_err(|err| { + tracing::error!(?err, "listing public objects"); + StatusCode::INTERNAL_SERVER_ERROR + })?; let total = db::catalog::count_public_objects(state.db.pool()) .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + .map_err(|err| { + tracing::error!(?err, "counting public objects"); + StatusCode::INTERNAL_SERVER_ERROR + })?; Ok(Json(PublicObjectPage { items: objects.iter().map(PublicView::from_object).collect(), @@ -106,7 +112,10 @@ pub(crate) async fn get_object( match db::catalog::public_object_by_id(state.db.pool(), object_id).await { Ok(Some(object)) => Json(PublicView::from_object(&object)).into_response(), Ok(None) => StatusCode::NOT_FOUND.into_response(), - Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), + Err(err) => { + tracing::error!(?err, "fetching public object"); + StatusCode::INTERNAL_SERVER_ERROR.into_response() + } } } diff --git a/crates/api/tests/admin_catalog.rs b/crates/api/tests/admin_catalog.rs index 9ef9e8a..d9e4594 100644 --- a/crates/api/tests/admin_catalog.rs +++ b/crates/api/tests/admin_catalog.rs @@ -263,3 +263,30 @@ async fn create_and_list_authorities_by_kind(pool: PgPool) { let bad = app2_get(&app, &cookie, "/api/admin/authorities?kind=alien").await; assert_eq!(bad, StatusCode::UNPROCESSABLE_ENTITY); } + +#[sqlx::test(migrations = "../db/migrations")] +async fn add_term_to_missing_vocabulary_is_404(pool: PgPool) { + migrate_sessions(&db::Db::from_pool(pool.clone())) + .await + .unwrap(); + + seed_user(&pool, "ed@example.com", "pw-editor-123", Role::Editor).await; + + let app = build_app(state(pool)); + let cookie = login(&app, "ed@example.com", "pw-editor-123").await; + + let resp = app + .oneshot( + Request::builder() + .method("POST") + .uri("/api/admin/vocabularies/00000000-0000-0000-0000-000000000000/terms") + .header(header::COOKIE, &cookie) + .header(header::CONTENT_TYPE, "application/json") + .body(Body::from(r#"{"labels":[{"lang":"en","label":"X"}]}"#)) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(resp.status(), StatusCode::NOT_FOUND); +}