fix(api): enable typed creation error hooks on generated Backend clients

This commit is contained in:
2026-11-06 10:35:14 +09:00
parent ba560ad226
commit 8d39122294
9 changed files with 139 additions and 10 deletions
+6 -3
View File
@@ -1909,8 +1909,9 @@ pub trait ServerApi {
"/api/workers",
status = 200,
error_status = 400,
additional_error_statuses = [401, 403, 404, 409, 500, 502, 503],
additional_error_statuses = [401, 403, 404, 409, 500, 502, 503, 504],
additional_error_content_types = ["application/problem+json"],
client_error_hook = true,
bearer_auth = true,
browser_auth = true
)]
@@ -1976,7 +1977,8 @@ pub trait ServerApi {
"/api/w/{workspace_id}/worker-control/workers",
status = 200,
error_status = 400,
additional_error_statuses = [401, 403, 404, 409, 500, 502, 503],
additional_error_statuses = [401, 403, 404, 409, 500, 502, 503, 504],
client_error_hook = true,
openapi = false,
)]
async fn worker_control_spawn(
@@ -2101,8 +2103,9 @@ pub trait ServerApi {
"/api/w/{workspace_id}/workers",
status = 200,
error_status = 400,
additional_error_statuses = [401, 403, 404, 409, 500, 502, 503],
additional_error_statuses = [401, 403, 404, 409, 500, 502, 503, 504],
additional_error_content_types = ["application/problem+json"],
client_error_hook = true,
bearer_auth = true,
browser_auth = true
)]
@@ -0,0 +1,96 @@
//! Generated Backend client → isolated loopback HTTP response → typed result.
//! The server response producer is substituted; authorization and persistence are
//! exercised by workspace-server's connected tests, not by this codec connection.
use runtime_api::startup::*;
use server_api::{CreateWorkspaceWorkerRequest, ServerApiClient, client_support, server_support};
use server_support::framework::{Body, Response, Router, post};
struct ServerTask(tokio::task::JoinHandle<()>);
impl Drop for ServerTask {
fn drop(&mut self) {
self.0.abort();
}
}
fn request() -> CreateWorkspaceWorkerRequest {
serde_json::from_value(serde_json::json!({
"runtime_id": "runtime", "display_name": "Worker", "feature_connections": {}
}))
.unwrap()
}
#[tokio::test]
async fn every_creation_client_decodes_actual_problem_status_and_operation_facts() {
let timeout = WorkerCreateFailure::from_cause(
StartupCause::Boundary(StartupBoundaryCause {
reason: StartupBoundaryReason::Timeout,
}),
StartupOperationFacts {
acceptance: CreationAcceptance::Unknown,
execution: ExecutionOutcome::Unknown,
finalize: None,
backend_compensation: None,
correlation: None,
},
)
.unwrap()
.problem(
StartupBoundary::Browser,
&SessionFailureVisibility::default(),
"",
)
.unwrap();
for (status, body) in [
(409, include_bytes!("../../runtime-api/tests/fixtures/startup-browser-session-record-denied.problem.json").to_vec()),
(502, include_bytes!("../../runtime-api/tests/fixtures/startup-browser-registration-response-lost.problem.json").to_vec()),
(504, timeout.into_body()),
] {
let DecodeStartupProblem::Known(expected) = decode_startup_problem(&body, status, PROBLEM_CONTENT_TYPE, StartupBoundary::Browser) else {
panic!("producer fixture must have a known typed cause");
};
let router = ["/api/workers", "/api/w/workspace/workers", "/api/w/workspace/worker-control/workers"]
.into_iter().fold(Router::new(), |router, path| {
let body = body.clone();
router.route(path, post(move || {
let body = body.clone();
async move { Response::builder().status(status).header("content-type", PROBLEM_CONTENT_TYPE).body(Body::from(body)).unwrap() }
}))
});
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
let _server = ServerTask(tokio::spawn(async move {
server_support::framework::serve(listener, router).await.unwrap();
}));
let client = ServerApiClient::builder(format!("http://{address}"))
.unwrap().request_timeout(std::time::Duration::from_secs(3)).build().unwrap();
for result in [
client.workspace_worker_create("workspace".into(), request()).await,
client.workspace_worker_create_alias(request()).await,
client.worker_control_spawn("workspace".into(), request()).await,
] {
let Err(client_support::ClientError::Public { status: actual_status, error }) = result else {
panic!("generated route must invoke the Problem decoder, not legacy Deserialize");
};
assert_eq!(actual_status.as_u16(), status);
let actual = error.failure().expect("retain typed non-HTTP failure");
assert_eq!(actual.startup().primary, expected.startup().primary);
assert_eq!(actual.public_facts(), expected.public_facts());
}
}
}
#[test]
fn creation_openapi_declares_gateway_timeout_with_both_error_representations() {
let document: serde_json::Value = serde_json::from_str(
&server_api::canonical_openapi_document()
.unwrap()
.to_json()
.unwrap(),
)
.unwrap();
for path in ["/api/workers", "/api/w/{workspace_id}/workers"] {
let content = &document["paths"][path]["post"]["responses"]["504"]["content"];
assert!(content.get("application/problem+json").is_some());
assert!(content.get("application/json").is_some());
}
}
+31 -1
View File
@@ -24199,7 +24199,7 @@
"info": {
"title": "Yoi Server API",
"version": "0.1.0",
"x-yoi-source-digest": "sha256:afde3903f29b9087390b69f9761a935cd5004c4dab17ce684f4149c4abae89c4"
"x-yoi-source-digest": "sha256:2dad54b31293643566259ea555e11c46effce33aab9f30c8a861a46303024bad"
},
"openapi": "3.1.0",
"paths": {
@@ -49679,6 +49679,21 @@
}
},
"description": "Error response"
},
"504": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RepositoryApiError"
}
},
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/StartupProblemDetails"
}
}
},
"description": "Error response"
}
},
"security": [
@@ -50936,6 +50951,21 @@
}
},
"description": "Error response"
},
"504": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RepositoryApiError"
}
},
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/StartupProblemDetails"
}
}
},
"description": "Error response"
}
},
"security": [
@@ -1,6 +1,6 @@
// @generated by yoi-companion-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:afde3903f29b9087390b69f9761a935cd5004c4dab17ce684f4149c4abae89c4)
// Input contract: 0.1.0 (sha256:2dad54b31293643566259ea555e11c46effce33aab9f30c8a861a46303024bad)
// Options: companion-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/companion-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_companion_api_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-repository-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:afde3903f29b9087390b69f9761a935cd5004c4dab17ce684f4149c4abae89c4)
// Input contract: 0.1.0 (sha256:2dad54b31293643566259ea555e11c46effce33aab9f30c8a861a46303024bad)
// Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output
// Output: web/workspace/src/lib/generated/repository-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-runtime-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:afde3903f29b9087390b69f9761a935cd5004c4dab17ce684f4149c4abae89c4)
// Input contract: 0.1.0 (sha256:2dad54b31293643566259ea555e11c46effce33aab9f30c8a861a46303024bad)
// Options: runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/runtime-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_runtime_api_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-ticket-browser-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:afde3903f29b9087390b69f9761a935cd5004c4dab17ce684f4149c4abae89c4)
// Input contract: 0.1.0 (sha256:2dad54b31293643566259ea555e11c46effce33aab9f30c8a861a46303024bad)
// Options: ticket-objective-merge-request-browser-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/ticket-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_ticket_api_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-workdir-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:afde3903f29b9087390b69f9761a935cd5004c4dab17ce684f4149c4abae89c4)
// Input contract: 0.1.0 (sha256:2dad54b31293643566259ea555e11c46effce33aab9f30c8a861a46303024bad)
// Options: workdir-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/workdir-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_workdir_api_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-worker-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:afde3903f29b9087390b69f9761a935cd5004c4dab17ce684f4149c4abae89c4)
// Input contract: 0.1.0 (sha256:2dad54b31293643566259ea555e11c46effce33aab9f30c8a861a46303024bad)
// Options: worker-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/worker-launch-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_worker_launch_api_types -- --check