api: finish structured error normalization
This commit is contained in:
@@ -2,18 +2,18 @@
|
|||||||
|
|
||||||
## Current
|
## Current
|
||||||
|
|
||||||
### `feat/error-structure-normalization`
|
### `feat/correlation-id-propagation`
|
||||||
|
|
||||||
Status: in_progress
|
Status: in_progress
|
||||||
|
|
||||||
DoD:
|
DoD:
|
||||||
- runtime and API errors expose structured context where stable fields exist
|
- admin-api, mcp-server, runtime, and adapters share a propagated correlation id
|
||||||
- secret/runtime failure classes are mappable without parsing ad hoc message text
|
- request-scoped logs can be joined across layers without guessing from timestamps
|
||||||
- opaque third-party details remain only where no stable structure exists
|
- outbound adapter calls include correlation metadata where transport allows it
|
||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
- `feat/correlation-id-propagation`
|
- `feat/runtime-rate-limiting-and-backpressure`
|
||||||
|
|
||||||
## Backlog
|
## Backlog
|
||||||
|
|
||||||
|
|||||||
@@ -69,20 +69,6 @@ impl ApiError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn not_found(message: impl Into<String>) -> Self {
|
|
||||||
Self::NotFound {
|
|
||||||
message: message.into(),
|
|
||||||
context: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn conflict(message: impl Into<String>) -> Self {
|
|
||||||
Self::Conflict {
|
|
||||||
message: message.into(),
|
|
||||||
context: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn internal(message: impl Into<String>) -> Self {
|
pub fn internal(message: impl Into<String>) -> Self {
|
||||||
Self::Internal {
|
Self::Internal {
|
||||||
message: message.into(),
|
message: message.into(),
|
||||||
|
|||||||
@@ -961,7 +961,12 @@ impl AdminService {
|
|||||||
.registry
|
.registry
|
||||||
.get_auth_user_by_id(user_id)
|
.get_auth_user_by_id(user_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| ApiError::not_found(format!("user {} was not found", user_id.as_str())))?
|
.ok_or_else(|| {
|
||||||
|
ApiError::not_found_with_context(
|
||||||
|
format!("user {} was not found", user_id.as_str()),
|
||||||
|
json!({ "user_id": user_id.as_str() }),
|
||||||
|
)
|
||||||
|
})?
|
||||||
.user;
|
.user;
|
||||||
let memberships = self.registry.list_workspaces_for_user(user_id).await?;
|
let memberships = self.registry.list_workspaces_for_user(user_id).await?;
|
||||||
|
|
||||||
@@ -988,7 +993,10 @@ impl AdminService {
|
|||||||
.get_auth_user_by_id(user_id)
|
.get_auth_user_by_id(user_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
ApiError::not_found(format!("user {} was not found", user_id.as_str()))
|
ApiError::not_found_with_context(
|
||||||
|
format!("user {} was not found", user_id.as_str()),
|
||||||
|
json!({ "user_id": user_id.as_str() }),
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if !verify_password(
|
if !verify_password(
|
||||||
@@ -1016,7 +1024,10 @@ impl AdminService {
|
|||||||
.get_workspace(workspace_id)
|
.get_workspace(workspace_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
ApiError::not_found(format!("workspace {} was not found", workspace_id.as_str()))
|
ApiError::not_found_with_context(
|
||||||
|
format!("workspace {} was not found", workspace_id.as_str()),
|
||||||
|
json!({ "workspace_id": workspace_id.as_str() }),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1071,11 +1082,17 @@ impl AdminService {
|
|||||||
.iter()
|
.iter()
|
||||||
.find(|membership| &membership.user.id == target_user_id)
|
.find(|membership| &membership.user.id == target_user_id)
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
ApiError::not_found(format!(
|
ApiError::not_found_with_context(
|
||||||
"membership for user {} in workspace {} was not found",
|
format!(
|
||||||
target_user_id.as_str(),
|
"membership for user {} in workspace {} was not found",
|
||||||
workspace_id.as_str()
|
target_user_id.as_str(),
|
||||||
))
|
workspace_id.as_str()
|
||||||
|
),
|
||||||
|
json!({
|
||||||
|
"workspace_id": workspace_id.as_str(),
|
||||||
|
"user_id": target_user_id.as_str(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if !matches!(
|
if !matches!(
|
||||||
@@ -1122,11 +1139,17 @@ impl AdminService {
|
|||||||
.iter()
|
.iter()
|
||||||
.find(|membership| &membership.user.id == target_user_id)
|
.find(|membership| &membership.user.id == target_user_id)
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
ApiError::not_found(format!(
|
ApiError::not_found_with_context(
|
||||||
"membership for user {} in workspace {} was not found",
|
format!(
|
||||||
target_user_id.as_str(),
|
"membership for user {} in workspace {} was not found",
|
||||||
workspace_id.as_str()
|
target_user_id.as_str(),
|
||||||
))
|
workspace_id.as_str()
|
||||||
|
),
|
||||||
|
json!({
|
||||||
|
"workspace_id": workspace_id.as_str(),
|
||||||
|
"user_id": target_user_id.as_str(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if !matches!(
|
if !matches!(
|
||||||
@@ -1826,10 +1849,10 @@ impl AdminService {
|
|||||||
.await?
|
.await?
|
||||||
.is_some()
|
.is_some()
|
||||||
{
|
{
|
||||||
return Err(ApiError::conflict(format!(
|
return Err(ApiError::conflict_with_context(
|
||||||
"operation with name {} already exists",
|
format!("operation with name {} already exists", payload.name),
|
||||||
payload.name
|
json!({ "name": payload.name }),
|
||||||
)));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let now = OffsetDateTime::now_utc();
|
let now = OffsetDateTime::now_utc();
|
||||||
@@ -1888,7 +1911,10 @@ impl AdminService {
|
|||||||
.get_operation_summary(workspace_id, operation_id)
|
.get_operation_summary(workspace_id, operation_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
ApiError::not_found(format!("operation {} was not found", operation_id.as_str()))
|
ApiError::not_found_with_context(
|
||||||
|
format!("operation {} was not found", operation_id.as_str()),
|
||||||
|
json!({ "operation_id": operation_id.as_str() }),
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
let now = OffsetDateTime::now_utc();
|
let now = OffsetDateTime::now_utc();
|
||||||
let version = summary.current_draft_version + 1;
|
let version = summary.current_draft_version + 1;
|
||||||
@@ -2757,10 +2783,10 @@ impl AdminService {
|
|||||||
.await?
|
.await?
|
||||||
.is_some()
|
.is_some()
|
||||||
{
|
{
|
||||||
return Err(ApiError::conflict(format!(
|
return Err(ApiError::conflict_with_context(
|
||||||
"agent with slug {} already exists",
|
format!("agent with slug {} already exists", payload.slug),
|
||||||
payload.slug
|
json!({ "slug": payload.slug }),
|
||||||
)));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let now = OffsetDateTime::now_utc();
|
let now = OffsetDateTime::now_utc();
|
||||||
@@ -2816,7 +2842,10 @@ impl AdminService {
|
|||||||
.get_agent_summary(workspace_id, agent_id)
|
.get_agent_summary(workspace_id, agent_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
ApiError::not_found(format!("agent {} was not found", agent_id.as_str()))
|
ApiError::not_found_with_context(
|
||||||
|
format!("agent {} was not found", agent_id.as_str()),
|
||||||
|
json!({ "agent_id": agent_id.as_str() }),
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if payload.slug != existing.slug
|
if payload.slug != existing.slug
|
||||||
@@ -2825,10 +2854,10 @@ impl AdminService {
|
|||||||
.await?
|
.await?
|
||||||
.is_some()
|
.is_some()
|
||||||
{
|
{
|
||||||
return Err(ApiError::conflict(format!(
|
return Err(ApiError::conflict_with_context(
|
||||||
"agent with slug {} already exists",
|
format!("agent with slug {} already exists", payload.slug),
|
||||||
payload.slug
|
json!({ "slug": payload.slug }),
|
||||||
)));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let updated_at = OffsetDateTime::now_utc();
|
let updated_at = OffsetDateTime::now_utc();
|
||||||
@@ -2861,7 +2890,10 @@ impl AdminService {
|
|||||||
.get_agent_summary(workspace_id, agent_id)
|
.get_agent_summary(workspace_id, agent_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
ApiError::not_found(format!("agent {} was not found", agent_id.as_str()))
|
ApiError::not_found_with_context(
|
||||||
|
format!("agent {} was not found", agent_id.as_str()),
|
||||||
|
json!({ "agent_id": agent_id.as_str() }),
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
self.registry.delete_agent(workspace_id, agent_id).await?;
|
self.registry.delete_agent(workspace_id, agent_id).await?;
|
||||||
@@ -4924,11 +4956,17 @@ fn ensure_stream_session_workspace(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(ApiError::not_found(format!(
|
Err(ApiError::not_found_with_context(
|
||||||
"stream session {} was not found in workspace {}",
|
format!(
|
||||||
session.id.as_str(),
|
"stream session {} was not found in workspace {}",
|
||||||
workspace_id.as_str()
|
session.id.as_str(),
|
||||||
)))
|
workspace_id.as_str()
|
||||||
|
),
|
||||||
|
json!({
|
||||||
|
"session_id": session.id.as_str(),
|
||||||
|
"workspace_id": workspace_id.as_str(),
|
||||||
|
}),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_async_job_workspace(
|
fn ensure_async_job_workspace(
|
||||||
@@ -4939,11 +4977,17 @@ fn ensure_async_job_workspace(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(ApiError::not_found(format!(
|
Err(ApiError::not_found_with_context(
|
||||||
"async job {} was not found in workspace {}",
|
format!(
|
||||||
job.id.as_str(),
|
"async job {} was not found in workspace {}",
|
||||||
workspace_id.as_str()
|
job.id.as_str(),
|
||||||
)))
|
workspace_id.as_str()
|
||||||
|
),
|
||||||
|
json!({
|
||||||
|
"job_id": job.id.as_str(),
|
||||||
|
"workspace_id": workspace_id.as_str(),
|
||||||
|
}),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn usage_window(period: UsagePeriod) -> Result<(UsagePeriod, String, UsageBucket), ApiError> {
|
fn usage_window(period: UsagePeriod) -> Result<(UsagePeriod, String, UsageBucket), ApiError> {
|
||||||
|
|||||||
Reference in New Issue
Block a user