From 3e68c41f4eb83b547a369f2df50efa93fa5d49df Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Wed, 4 Mar 2026 21:32:24 -0500 Subject: [PATCH] Adopt uniform error response format across Arc API All API errors now return a consistent JSON shape: {"errors": [{"status": "4xx", "title": "...", "detail": "..."}]} Introduces ApiError type with IntoResponse impl, replaces bare StatusCode returns and ad-hoc {"error": "..."} responses in all handlers and auth extractors. Updates OpenAPI spec and regenerates TypeScript client. Co-Authored-By: Claude Opus 4.6 --- crates/arc-api/src/demo/mod.rs | 25 ++-- crates/arc-api/src/error.rs | 67 +++++++++ crates/arc-api/src/jwt_auth.rs | 43 +++--- crates/arc-api/src/lib.rs | 1 + crates/arc-api/src/server.rs | 61 +++----- docs/api-reference/arc-api.yaml | 138 +++++++++++++++++- openapi/arc-api.yaml | 138 +++++++++++++++++- .../src/.openapi-generator/FILES | 1 + .../arc-api-client/src/api/discovery-api.ts | 2 + .../arc-api-client/src/api/insights-api.ts | 3 + .../arc-api-client/src/api/projects-api.ts | 2 + packages/arc-api-client/src/api/retros-api.ts | 2 + .../src/api/run-internals-api.ts | 4 +- .../arc-api-client/src/api/run-outputs-api.ts | 2 + packages/arc-api-client/src/api/runs-api.ts | 2 +- .../arc-api-client/src/api/sessions-api.ts | 4 +- .../src/api/verifications-api.ts | 2 + .../arc-api-client/src/api/workflows-api.ts | 2 + .../src/models/error-response-entry.ts | 22 +++ .../src/models/error-response.ts | 5 +- packages/arc-api-client/src/models/index.ts | 1 + 21 files changed, 439 insertions(+), 88 deletions(-) create mode 100644 crates/arc-api/src/error.rs create mode 100644 packages/arc-api-client/src/models/error-response-entry.ts diff --git a/crates/arc-api/src/demo/mod.rs b/crates/arc-api/src/demo/mod.rs index fa541948a..9fdcb1689 100644 --- a/crates/arc-api/src/demo/mod.rs +++ b/crates/arc-api/src/demo/mod.rs @@ -8,6 +8,7 @@ use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use axum::Json; +use crate::error::ApiError; use crate::jwt_auth::AuthenticatedService; use crate::server::AppState; @@ -126,7 +127,7 @@ pub async fn get_run_status( }), ) .into_response(), - None => StatusCode::NOT_FOUND.into_response(), + None => ApiError::not_found("Run not found.").into_response(), } } @@ -152,7 +153,7 @@ pub async fn run_events_stub( State(_state): State>, Path(_id): Path, ) -> Response { - StatusCode::GONE.into_response() + ApiError::new(StatusCode::GONE, "Event stream closed.").into_response() } pub async fn checkpoint_stub( @@ -196,11 +197,7 @@ pub async fn get_run_graph( { Ok(child) => child, Err(_) => { - return ( - StatusCode::BAD_GATEWAY, - Json(serde_json::json!({"error": "graphviz dot command not available"})), - ) - .into_response(); + return ApiError::new(StatusCode::BAD_GATEWAY, "Graphviz dot command not available.").into_response(); } }; @@ -216,11 +213,7 @@ pub async fn get_run_graph( output.stdout, ) .into_response(), - _ => ( - StatusCode::BAD_GATEWAY, - Json(serde_json::json!({"error": "dot rendering failed"})), - ) - .into_response(), + _ => ApiError::new(StatusCode::BAD_GATEWAY, "Dot rendering failed.").into_response(), } } @@ -417,7 +410,7 @@ pub async fn get_workflow( ) -> Response { match workflows::detail(&name) { Some(detail) => (StatusCode::OK, Json(detail)).into_response(), - None => StatusCode::NOT_FOUND.into_response(), + None => ApiError::not_found("Workflow not found.").into_response(), } } @@ -461,7 +454,7 @@ pub async fn get_verification_detail( ) -> Response { match verifications::detail(&slug) { Some(detail) => (StatusCode::OK, Json(detail)).into_response(), - None => StatusCode::NOT_FOUND.into_response(), + None => ApiError::not_found("Verification not found.").into_response(), } } @@ -501,7 +494,7 @@ pub async fn get_session( ) -> Response { match sessions::detail(&id) { Some(detail) => (StatusCode::OK, Json(detail)).into_response(), - None => StatusCode::NOT_FOUND.into_response(), + None => ApiError::not_found("Session not found.").into_response(), } } @@ -519,7 +512,7 @@ pub async fn session_events_stub( Path(_id): Path, ) -> Response { // Return an empty SSE-like response - StatusCode::GONE.into_response() + ApiError::new(StatusCode::GONE, "Event stream closed.").into_response() } // ── Insights ─────────────────────────────────────────────────────────── diff --git a/crates/arc-api/src/error.rs b/crates/arc-api/src/error.rs new file mode 100644 index 000000000..0278f0cff --- /dev/null +++ b/crates/arc-api/src/error.rs @@ -0,0 +1,67 @@ +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use axum::Json; +use serde::Serialize; + +#[derive(Serialize)] +struct ErrorEntry { + status: String, + title: String, + detail: String, +} + +#[derive(Serialize)] +struct ErrorBody { + errors: Vec, +} + +/// Uniform API error response. +/// +/// Serializes to `{"errors": [{"status": "4xx", "title": "...", "detail": "..."}]}`. +pub struct ApiError { + status: StatusCode, + detail: String, +} + +impl ApiError { + pub fn new(status: StatusCode, detail: impl Into) -> Self { + Self { + status, + detail: detail.into(), + } + } + + pub fn not_found(detail: impl Into) -> Self { + Self::new(StatusCode::NOT_FOUND, detail) + } + + pub fn bad_request(detail: impl Into) -> Self { + Self::new(StatusCode::BAD_REQUEST, detail) + } + + pub fn unauthorized() -> Self { + Self::new(StatusCode::UNAUTHORIZED, "Authentication required.") + } + + pub fn forbidden() -> Self { + Self::new(StatusCode::FORBIDDEN, "Access denied.") + } +} + +impl IntoResponse for ApiError { + fn into_response(self) -> Response { + let title = self + .status + .canonical_reason() + .unwrap_or("Unknown") + .to_string(); + let body = ErrorBody { + errors: vec![ErrorEntry { + status: self.status.as_u16().to_string(), + title, + detail: self.detail, + }], + }; + (self.status, Json(body)).into_response() + } +} diff --git a/crates/arc-api/src/jwt_auth.rs b/crates/arc-api/src/jwt_auth.rs index 7458db7fc..742cc5f60 100644 --- a/crates/arc-api/src/jwt_auth.rs +++ b/crates/arc-api/src/jwt_auth.rs @@ -2,12 +2,13 @@ use std::sync::Arc; use axum::extract::FromRequestParts; use axum::http::request::Parts; -use axum::http::StatusCode; use jsonwebtoken::{Algorithm, DecodingKey, Validation}; use rustls_pki_types::CertificateDer; use serde::Deserialize; use tracing::warn; +use crate::error::ApiError; + /// JWT claims for service-to-service authentication. #[derive(Debug, Deserialize)] struct Claims { @@ -148,23 +149,23 @@ fn try_jwt( key: &DecodingKey, validation: &Validation, allowed_usernames: &[String], -) -> Result<(), StatusCode> { +) -> Result<(), ApiError> { let header = parts .headers .get("authorization") .and_then(|v| v.to_str().ok()) - .ok_or(StatusCode::UNAUTHORIZED)?; + .ok_or_else(ApiError::unauthorized)?; let token = header .strip_prefix("Bearer ") - .ok_or(StatusCode::UNAUTHORIZED)?; + .ok_or_else(ApiError::unauthorized)?; let token_data = jsonwebtoken::decode::(token, key, validation) - .map_err(|_| StatusCode::UNAUTHORIZED)?; + .map_err(|_| ApiError::unauthorized())?; // Fail closed: if no usernames are allowed, reject all requests if allowed_usernames.is_empty() { - return Err(StatusCode::FORBIDDEN); + return Err(ApiError::forbidden()); } // Extract GitHub username from sub claim URL (last path segment) @@ -173,38 +174,38 @@ fn try_jwt( .sub .as_deref() .and_then(|s| s.rsplit('/').next()) - .ok_or(StatusCode::FORBIDDEN)?; + .ok_or_else(ApiError::forbidden)?; if !allowed_usernames.iter().any(|u| u == username) { - return Err(StatusCode::FORBIDDEN); + return Err(ApiError::forbidden()); } Ok(()) } /// Try to authenticate via mTLS peer certificates. -fn try_mtls(parts: &Parts) -> Result<(), StatusCode> { +fn try_mtls(parts: &Parts) -> Result<(), ApiError> { let peer_certs = parts .extensions .get::() .and_then(|pc| pc.0.as_ref()) - .ok_or(StatusCode::UNAUTHORIZED)?; + .ok_or_else(ApiError::unauthorized)?; if peer_certs.is_empty() { - return Err(StatusCode::UNAUTHORIZED); + return Err(ApiError::unauthorized()); } // Verify we can parse the leaf certificate and extract a CN let cert = &peer_certs[0]; let (_, parsed) = - x509_parser::parse_x509_certificate(cert).map_err(|_| StatusCode::UNAUTHORIZED)?; + x509_parser::parse_x509_certificate(cert).map_err(|_| ApiError::unauthorized())?; parsed .subject() .iter_common_name() .next() .and_then(|cn| cn.as_str().ok()) - .ok_or(StatusCode::UNAUTHORIZED)?; + .ok_or_else(ApiError::unauthorized)?; Ok(()) } @@ -217,7 +218,7 @@ fn try_mtls(parts: &Parts) -> Result<(), StatusCode> { pub struct AuthenticatedService; impl FromRequestParts for AuthenticatedService { - type Rejection = StatusCode; + type Rejection = ApiError; async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { let auth_mode = parts @@ -231,10 +232,10 @@ impl FromRequestParts for AuthenticatedService { }; if strategies.is_empty() { - return Err(StatusCode::UNAUTHORIZED); + return Err(ApiError::unauthorized()); } - let mut last_err = StatusCode::UNAUTHORIZED; + let mut last_err = ApiError::unauthorized(); for strategy in strategies { let result = match strategy { @@ -265,7 +266,7 @@ pub struct AuthenticatedUser { } impl FromRequestParts for AuthenticatedUser { - type Rejection = StatusCode; + type Rejection = ApiError; async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { let auth_mode = parts @@ -283,10 +284,10 @@ impl FromRequestParts for AuthenticatedUser { }; if strategies.is_empty() { - return Err(StatusCode::UNAUTHORIZED); + return Err(ApiError::unauthorized()); } - let mut last_err = StatusCode::UNAUTHORIZED; + let mut last_err = ApiError::unauthorized(); for strategy in strategies { match strategy { @@ -300,7 +301,7 @@ impl FromRequestParts for AuthenticatedUser { return Ok(AuthenticatedUser { login }); } } - last_err = StatusCode::UNAUTHORIZED; + last_err = ApiError::unauthorized(); } AuthStrategy::Mtls => { if try_mtls(parts).is_ok() { @@ -308,7 +309,7 @@ impl FromRequestParts for AuthenticatedUser { return Ok(AuthenticatedUser { login }); } } - last_err = StatusCode::UNAUTHORIZED; + last_err = ApiError::unauthorized(); } } } diff --git a/crates/arc-api/src/lib.rs b/crates/arc-api/src/lib.rs index f6f28521f..c0ba947ee 100644 --- a/crates/arc-api/src/lib.rs +++ b/crates/arc-api/src/lib.rs @@ -1,4 +1,5 @@ mod demo; +pub mod error; pub mod jwt_auth; pub mod serve; pub mod server; diff --git a/crates/arc-api/src/server.rs b/crates/arc-api/src/server.rs index 831f9f3a5..00e49e959 100644 --- a/crates/arc-api/src/server.rs +++ b/crates/arc-api/src/server.rs @@ -16,6 +16,7 @@ use tracing::{error, info}; use arc_agent::LocalSandbox; +use crate::error::ApiError; use crate::jwt_auth::{AuthMode, AuthenticatedService, AuthenticatedUser}; use arc_workflows::checkpoint::Checkpoint; use arc_workflows::context::Context; @@ -191,7 +192,7 @@ pub fn build_router(state: Arc, auth_mode: AuthMode) -> Router { } async fn not_implemented() -> Response { - StatusCode::NOT_IMPLEMENTED.into_response() + ApiError::new(StatusCode::NOT_IMPLEMENTED, "Not implemented.").into_response() } async fn root() -> Response { @@ -268,11 +269,7 @@ async fn start_run( let graph = match arc_workflows::workflow::prepare_workflow(&req.dot_source) { Ok(g) => g, Err(e) => { - return ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({"error": e.to_string()})), - ) - .into_response(); + return ApiError::bad_request(e.to_string()).into_response(); } }; @@ -416,7 +413,7 @@ async fn get_run_status( }), ) .into_response(), - None => StatusCode::NOT_FOUND.into_response(), + None => ApiError::not_found("Run not found.").into_response(), } } @@ -449,7 +446,7 @@ async fn get_questions( .collect(); (StatusCode::OK, Json(questions)).into_response() } - None => StatusCode::NOT_FOUND.into_response(), + None => ApiError::not_found("Run not found.").into_response(), } } @@ -474,11 +471,7 @@ async fn submit_answer( match option { Some(opt) => Answer::selected(key.clone(), opt), None => { - return ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({"error": "invalid option key"})), - ) - .into_response(); + return ApiError::bad_request("Invalid option key.").into_response(); } } } @@ -487,7 +480,7 @@ async fn submit_answer( let accepted = managed_run.interviewer.submit_answer(&qid, answer); (StatusCode::OK, Json(SubmitAnswerResponse { accepted })).into_response() } - None => StatusCode::NOT_FOUND.into_response(), + None => ApiError::not_found("Run not found.").into_response(), } } @@ -501,9 +494,9 @@ async fn get_events( match runs.get(&id) { Some(managed_run) => match &managed_run.event_tx { Some(tx) => tx.subscribe(), - None => return StatusCode::GONE.into_response(), + None => return ApiError::new(StatusCode::GONE, "Event stream closed.").into_response(), }, - None => return StatusCode::NOT_FOUND.into_response(), + None => return ApiError::not_found("Run not found.").into_response(), } }; @@ -532,7 +525,7 @@ async fn get_checkpoint( Some(cp) => (StatusCode::OK, Json(cp.clone())).into_response(), None => (StatusCode::OK, Json(serde_json::json!(null))).into_response(), }, - None => StatusCode::NOT_FOUND.into_response(), + None => ApiError::not_found("Run not found.").into_response(), } } @@ -547,7 +540,7 @@ async fn get_context( Some(ctx) => (StatusCode::OK, Json(ctx.snapshot())).into_response(), None => (StatusCode::OK, Json(serde_json::json!({}))).into_response(), }, - None => StatusCode::NOT_FOUND.into_response(), + None => ApiError::not_found("Run not found.").into_response(), } } @@ -560,11 +553,7 @@ async fn cancel_run( match runs.get_mut(&id) { Some(managed_run) => { if managed_run.status != RunStatus::Running { - return ( - StatusCode::CONFLICT, - Json(serde_json::json!({"error": "run is not running"})), - ) - .into_response(); + return ApiError::new(StatusCode::CONFLICT, "Run is not running.").into_response(); } managed_run.cancel_token.store(true, Ordering::Relaxed); if let Some(cancel_tx) = managed_run.cancel_tx.take() { @@ -573,7 +562,7 @@ async fn cancel_run( managed_run.status = RunStatus::Cancelled; (StatusCode::OK, Json(serde_json::json!({"cancelled": true}))).into_response() } - None => StatusCode::NOT_FOUND.into_response(), + None => ApiError::not_found("Run not found.").into_response(), } } @@ -586,7 +575,7 @@ async fn get_retro( let runs = state.runs.lock().expect("runs lock poisoned"); match runs.get(&id) { Some(managed_run) => managed_run.logs_root.clone(), - None => return StatusCode::NOT_FOUND.into_response(), + None => return ApiError::not_found("Run not found.").into_response(), } }; @@ -609,7 +598,7 @@ async fn get_graph( let runs = state.runs.lock().expect("runs lock poisoned"); match runs.get(&id) { Some(managed_run) => managed_run.dot_source.clone(), - None => return StatusCode::NOT_FOUND.into_response(), + None => return ApiError::not_found("Run not found.").into_response(), } }; @@ -622,11 +611,7 @@ async fn get_graph( { Ok(child) => child, Err(_) => { - return ( - StatusCode::BAD_GATEWAY, - Json(serde_json::json!({"error": "graphviz dot command not available"})), - ) - .into_response(); + return ApiError::new(StatusCode::BAD_GATEWAY, "Graphviz dot command not available.").into_response(); } }; @@ -645,17 +630,11 @@ async fn get_graph( .into_response(), Ok(output) => { let stderr = String::from_utf8_lossy(&output.stderr); - ( - StatusCode::BAD_GATEWAY, - Json(serde_json::json!({"error": format!("dot failed: {stderr}")})), - ) - .into_response() + ApiError::new(StatusCode::BAD_GATEWAY, format!("dot failed: {stderr}")).into_response() + } + Err(e) => { + ApiError::new(StatusCode::BAD_GATEWAY, format!("dot process error: {e}")).into_response() } - Err(e) => ( - StatusCode::BAD_GATEWAY, - Json(serde_json::json!({"error": format!("dot process error: {e}")})), - ) - .into_response(), } } diff --git a/docs/api-reference/arc-api.yaml b/docs/api-reference/arc-api.yaml index 7fbd20b51..14eb3c636 100644 --- a/docs/api-reference/arc-api.yaml +++ b/docs/api-reference/arc-api.yaml @@ -91,6 +91,10 @@ paths: $ref: "#/components/schemas/UserResponse" "401": description: Not authenticated + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" # ── Runs ────────────────────────────────────────────────────────────── @@ -148,6 +152,10 @@ paths: $ref: "#/components/schemas/RunStatusResponse" "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /runs/{id}/cancel: post: @@ -170,6 +178,10 @@ paths: - cancelled "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" "409": description: Run is not running content: @@ -193,6 +205,10 @@ paths: type: string "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" "502": description: Graphviz not available content: @@ -215,6 +231,10 @@ paths: schema: {} "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /runs/{id}/context: get: @@ -232,6 +252,10 @@ paths: type: object "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /runs/{id}/events: get: @@ -249,8 +273,16 @@ paths: type: string "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" "410": description: Event stream closed + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /runs/{id}/questions: get: @@ -270,6 +302,10 @@ paths: $ref: "#/components/schemas/ApiQuestion" "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /runs/{id}/questions/{qid}/answer: post: @@ -304,6 +340,10 @@ paths: $ref: "#/components/schemas/ErrorResponse" "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /runs/{id}/retro: get: @@ -320,6 +360,10 @@ paths: schema: {} "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /runs/{id}/stages: get: @@ -339,6 +383,10 @@ paths: $ref: "#/components/schemas/RunStage" "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /runs/{id}/stages/{stageId}/turns: get: @@ -363,6 +411,10 @@ paths: $ref: "#/components/schemas/StageTurn" "404": description: Run or stage not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /runs/{id}/files: get: @@ -385,6 +437,10 @@ paths: $ref: "#/components/schemas/RunFiles" "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /runs/{id}/usage: get: @@ -402,6 +458,10 @@ paths: $ref: "#/components/schemas/RunUsage" "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /runs/{id}/verifications: get: @@ -421,6 +481,10 @@ paths: $ref: "#/components/schemas/RunVerification" "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /runs/{id}/configuration: get: @@ -438,6 +502,10 @@ paths: type: string "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /runs/{id}/steer: post: @@ -466,6 +534,10 @@ paths: - accepted "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /runs/{id}/preview: post: @@ -489,6 +561,10 @@ paths: $ref: "#/components/schemas/PreviewUrlResponse" "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" # ── Workflows ───────────────────────────────────────────────────────── @@ -527,6 +603,10 @@ paths: $ref: "#/components/schemas/WorkflowDetail" "404": description: Workflow not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /workflows/{name}/runs: get: @@ -550,6 +630,10 @@ paths: $ref: "#/components/schemas/RunListItem" "404": description: Workflow not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" post: operationId: startWorkflowRun tags: [Workflows] @@ -569,6 +653,10 @@ paths: $ref: "#/components/schemas/StartRunResponse" "404": description: Workflow not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" # ── Verifications ───────────────────────────────────────────────────── @@ -607,6 +695,10 @@ paths: $ref: "#/components/schemas/VerificationDetailResponse" "404": description: Control not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" # ── Retros ──────────────────────────────────────────────────────────── @@ -679,6 +771,10 @@ paths: $ref: "#/components/schemas/SessionDetail" "404": description: Session not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /sessions/{id}/messages: post: @@ -711,6 +807,10 @@ paths: - accepted "404": description: Session not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /sessions/{id}/events: get: @@ -732,6 +832,10 @@ paths: type: string "404": description: Session not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" # ── Insights ────────────────────────────────────────────────────────── @@ -793,6 +897,10 @@ paths: $ref: "#/components/schemas/SavedQuery" "404": description: Query not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" delete: operationId: deleteSavedQuery tags: [Insights] @@ -808,6 +916,10 @@ paths: description: Query deleted "404": description: Query not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /insights/execute: post: @@ -899,6 +1011,10 @@ paths: $ref: "#/components/schemas/Branch" "404": description: Project not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" components: parameters: @@ -1000,13 +1116,29 @@ components: accepted: type: boolean + ErrorResponseEntry: + type: object + required: + - status + - title + - detail + properties: + status: + type: string + title: + type: string + detail: + type: string + ErrorResponse: type: object required: - - error + - errors properties: - error: - type: string + errors: + type: array + items: + $ref: "#/components/schemas/ErrorResponseEntry" # ── New Run Schemas ───────────────────────────────────────────────── diff --git a/openapi/arc-api.yaml b/openapi/arc-api.yaml index 7fbd20b51..14eb3c636 100644 --- a/openapi/arc-api.yaml +++ b/openapi/arc-api.yaml @@ -91,6 +91,10 @@ paths: $ref: "#/components/schemas/UserResponse" "401": description: Not authenticated + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" # ── Runs ────────────────────────────────────────────────────────────── @@ -148,6 +152,10 @@ paths: $ref: "#/components/schemas/RunStatusResponse" "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /runs/{id}/cancel: post: @@ -170,6 +178,10 @@ paths: - cancelled "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" "409": description: Run is not running content: @@ -193,6 +205,10 @@ paths: type: string "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" "502": description: Graphviz not available content: @@ -215,6 +231,10 @@ paths: schema: {} "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /runs/{id}/context: get: @@ -232,6 +252,10 @@ paths: type: object "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /runs/{id}/events: get: @@ -249,8 +273,16 @@ paths: type: string "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" "410": description: Event stream closed + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /runs/{id}/questions: get: @@ -270,6 +302,10 @@ paths: $ref: "#/components/schemas/ApiQuestion" "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /runs/{id}/questions/{qid}/answer: post: @@ -304,6 +340,10 @@ paths: $ref: "#/components/schemas/ErrorResponse" "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /runs/{id}/retro: get: @@ -320,6 +360,10 @@ paths: schema: {} "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /runs/{id}/stages: get: @@ -339,6 +383,10 @@ paths: $ref: "#/components/schemas/RunStage" "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /runs/{id}/stages/{stageId}/turns: get: @@ -363,6 +411,10 @@ paths: $ref: "#/components/schemas/StageTurn" "404": description: Run or stage not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /runs/{id}/files: get: @@ -385,6 +437,10 @@ paths: $ref: "#/components/schemas/RunFiles" "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /runs/{id}/usage: get: @@ -402,6 +458,10 @@ paths: $ref: "#/components/schemas/RunUsage" "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /runs/{id}/verifications: get: @@ -421,6 +481,10 @@ paths: $ref: "#/components/schemas/RunVerification" "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /runs/{id}/configuration: get: @@ -438,6 +502,10 @@ paths: type: string "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /runs/{id}/steer: post: @@ -466,6 +534,10 @@ paths: - accepted "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /runs/{id}/preview: post: @@ -489,6 +561,10 @@ paths: $ref: "#/components/schemas/PreviewUrlResponse" "404": description: Run not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" # ── Workflows ───────────────────────────────────────────────────────── @@ -527,6 +603,10 @@ paths: $ref: "#/components/schemas/WorkflowDetail" "404": description: Workflow not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /workflows/{name}/runs: get: @@ -550,6 +630,10 @@ paths: $ref: "#/components/schemas/RunListItem" "404": description: Workflow not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" post: operationId: startWorkflowRun tags: [Workflows] @@ -569,6 +653,10 @@ paths: $ref: "#/components/schemas/StartRunResponse" "404": description: Workflow not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" # ── Verifications ───────────────────────────────────────────────────── @@ -607,6 +695,10 @@ paths: $ref: "#/components/schemas/VerificationDetailResponse" "404": description: Control not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" # ── Retros ──────────────────────────────────────────────────────────── @@ -679,6 +771,10 @@ paths: $ref: "#/components/schemas/SessionDetail" "404": description: Session not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /sessions/{id}/messages: post: @@ -711,6 +807,10 @@ paths: - accepted "404": description: Session not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /sessions/{id}/events: get: @@ -732,6 +832,10 @@ paths: type: string "404": description: Session not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" # ── Insights ────────────────────────────────────────────────────────── @@ -793,6 +897,10 @@ paths: $ref: "#/components/schemas/SavedQuery" "404": description: Query not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" delete: operationId: deleteSavedQuery tags: [Insights] @@ -808,6 +916,10 @@ paths: description: Query deleted "404": description: Query not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /insights/execute: post: @@ -899,6 +1011,10 @@ paths: $ref: "#/components/schemas/Branch" "404": description: Project not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" components: parameters: @@ -1000,13 +1116,29 @@ components: accepted: type: boolean + ErrorResponseEntry: + type: object + required: + - status + - title + - detail + properties: + status: + type: string + title: + type: string + detail: + type: string + ErrorResponse: type: object required: - - error + - errors properties: - error: - type: string + errors: + type: array + items: + $ref: "#/components/schemas/ErrorResponseEntry" # ── New Run Schemas ───────────────────────────────────────────────── diff --git a/packages/arc-api-client/src/.openapi-generator/FILES b/packages/arc-api-client/src/.openapi-generator/FILES index 64f485a93..916a2e235 100644 --- a/packages/arc-api-client/src/.openapi-generator/FILES +++ b/packages/arc-api-client/src/.openapi-generator/FILES @@ -28,6 +28,7 @@ models/create-session-request.ts models/create-session-response.ts models/diff-file.ts models/diff-stats.ts +models/error-response-entry.ts models/error-response.ts models/evaluation-result.ts models/execute-query-request.ts diff --git a/packages/arc-api-client/src/api/discovery-api.ts b/packages/arc-api-client/src/api/discovery-api.ts index 22ce54e92..1aac7271e 100644 --- a/packages/arc-api-client/src/api/discovery-api.ts +++ b/packages/arc-api-client/src/api/discovery-api.ts @@ -22,6 +22,8 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base'; // @ts-ignore +import type { ErrorResponse } from '../models'; +// @ts-ignore import type { HealthResponse } from '../models'; // @ts-ignore import type { RootResponse } from '../models'; diff --git a/packages/arc-api-client/src/api/insights-api.ts b/packages/arc-api-client/src/api/insights-api.ts index a46027684..699775871 100644 --- a/packages/arc-api-client/src/api/insights-api.ts +++ b/packages/arc-api-client/src/api/insights-api.ts @@ -22,6 +22,8 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base'; // @ts-ignore +import type { ErrorResponse } from '../models'; +// @ts-ignore import type { ExecuteQueryRequest } from '../models'; // @ts-ignore import type { ExecuteQueryResponse } from '../models'; @@ -94,6 +96,7 @@ export const InsightsApiAxiosParamCreator = function (configuration?: Configurat const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; + localVarHeaderParameter['Accept'] = 'application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; diff --git a/packages/arc-api-client/src/api/projects-api.ts b/packages/arc-api-client/src/api/projects-api.ts index 02b634ad0..779acbcfc 100644 --- a/packages/arc-api-client/src/api/projects-api.ts +++ b/packages/arc-api-client/src/api/projects-api.ts @@ -24,6 +24,8 @@ import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError // @ts-ignore import type { Branch } from '../models'; // @ts-ignore +import type { ErrorResponse } from '../models'; +// @ts-ignore import type { Project } from '../models'; /** * ProjectsApi - axios parameter creator diff --git a/packages/arc-api-client/src/api/retros-api.ts b/packages/arc-api-client/src/api/retros-api.ts index f8eceef1c..9d6c00bba 100644 --- a/packages/arc-api-client/src/api/retros-api.ts +++ b/packages/arc-api-client/src/api/retros-api.ts @@ -22,6 +22,8 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base'; // @ts-ignore +import type { ErrorResponse } from '../models'; +// @ts-ignore import type { RetroListItem } from '../models'; /** * RetrosApi - axios parameter creator diff --git a/packages/arc-api-client/src/api/run-internals-api.ts b/packages/arc-api-client/src/api/run-internals-api.ts index 9e8566df7..c7f610406 100644 --- a/packages/arc-api-client/src/api/run-internals-api.ts +++ b/packages/arc-api-client/src/api/run-internals-api.ts @@ -22,6 +22,8 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base'; // @ts-ignore +import type { ErrorResponse } from '../models'; +// @ts-ignore import type { RunStage } from '../models'; // @ts-ignore import type { StageTurn } from '../models'; @@ -159,7 +161,7 @@ export const RunInternalsApiAxiosParamCreator = function (configuration?: Config const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; - localVarHeaderParameter['Accept'] = 'text/plain'; + localVarHeaderParameter['Accept'] = 'text/plain,application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; diff --git a/packages/arc-api-client/src/api/run-outputs-api.ts b/packages/arc-api-client/src/api/run-outputs-api.ts index 5c733d1dd..77150bceb 100644 --- a/packages/arc-api-client/src/api/run-outputs-api.ts +++ b/packages/arc-api-client/src/api/run-outputs-api.ts @@ -22,6 +22,8 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base'; // @ts-ignore +import type { ErrorResponse } from '../models'; +// @ts-ignore import type { RunFiles } from '../models'; // @ts-ignore import type { RunUsage } from '../models'; diff --git a/packages/arc-api-client/src/api/runs-api.ts b/packages/arc-api-client/src/api/runs-api.ts index ff49aa173..d66a0b4fe 100644 --- a/packages/arc-api-client/src/api/runs-api.ts +++ b/packages/arc-api-client/src/api/runs-api.ts @@ -228,7 +228,7 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration) const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; - localVarHeaderParameter['Accept'] = 'text/event-stream'; + localVarHeaderParameter['Accept'] = 'text/event-stream,application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; diff --git a/packages/arc-api-client/src/api/sessions-api.ts b/packages/arc-api-client/src/api/sessions-api.ts index 98f7f4034..0c2386f2f 100644 --- a/packages/arc-api-client/src/api/sessions-api.ts +++ b/packages/arc-api-client/src/api/sessions-api.ts @@ -26,6 +26,8 @@ import type { CreateSessionRequest } from '../models'; // @ts-ignore import type { CreateSessionResponse } from '../models'; // @ts-ignore +import type { ErrorResponse } from '../models'; +// @ts-ignore import type { SendMessageRequest } from '../models'; // @ts-ignore import type { SessionDetail } from '../models'; @@ -199,7 +201,7 @@ export const SessionsApiAxiosParamCreator = function (configuration?: Configurat const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; - localVarHeaderParameter['Accept'] = 'text/event-stream'; + localVarHeaderParameter['Accept'] = 'text/event-stream,application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; diff --git a/packages/arc-api-client/src/api/verifications-api.ts b/packages/arc-api-client/src/api/verifications-api.ts index 56f5805ed..144eba2cd 100644 --- a/packages/arc-api-client/src/api/verifications-api.ts +++ b/packages/arc-api-client/src/api/verifications-api.ts @@ -22,6 +22,8 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base'; // @ts-ignore +import type { ErrorResponse } from '../models'; +// @ts-ignore import type { VerificationCategory } from '../models'; // @ts-ignore import type { VerificationDetailResponse } from '../models'; diff --git a/packages/arc-api-client/src/api/workflows-api.ts b/packages/arc-api-client/src/api/workflows-api.ts index b360d3be8..69f7b6614 100644 --- a/packages/arc-api-client/src/api/workflows-api.ts +++ b/packages/arc-api-client/src/api/workflows-api.ts @@ -22,6 +22,8 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base'; // @ts-ignore +import type { ErrorResponse } from '../models'; +// @ts-ignore import type { RunListItem } from '../models'; // @ts-ignore import type { StartRunResponse } from '../models'; diff --git a/packages/arc-api-client/src/models/error-response-entry.ts b/packages/arc-api-client/src/models/error-response-entry.ts new file mode 100644 index 000000000..dd9aa0c96 --- /dev/null +++ b/packages/arc-api-client/src/models/error-response-entry.ts @@ -0,0 +1,22 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Arc Run API + * HTTP API for managing Arc workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface ErrorResponseEntry { + 'status': string; + 'title': string; + 'detail': string; +} + diff --git a/packages/arc-api-client/src/models/error-response.ts b/packages/arc-api-client/src/models/error-response.ts index 567f11078..998d38b1d 100644 --- a/packages/arc-api-client/src/models/error-response.ts +++ b/packages/arc-api-client/src/models/error-response.ts @@ -13,8 +13,11 @@ */ +// May contain unused imports in some cases +// @ts-ignore +import type { ErrorResponseEntry } from './error-response-entry'; export interface ErrorResponse { - 'error': string; + 'errors': Array; } diff --git a/packages/arc-api-client/src/models/index.ts b/packages/arc-api-client/src/models/index.ts index 1833ea26d..a2f454590 100644 --- a/packages/arc-api-client/src/models/index.ts +++ b/packages/arc-api-client/src/models/index.ts @@ -12,6 +12,7 @@ export * from './create-session-response'; export * from './diff-file'; export * from './diff-stats'; export * from './error-response'; +export * from './error-response-entry'; export * from './evaluation-result'; export * from './execute-query-request'; export * from './execute-query-response';