Skip to main content

runtara_core/server/
http_server.rs

1// Copyright (C) 2025 SyncMyOrders Sp. z o.o.
2// SPDX-License-Identifier: AGPL-3.0-or-later
3//! HTTP server for the instance protocol.
4//!
5//! Provides instance protocol operations over HTTP/JSON.
6//! This enables scenarios (native or WASM) using the HTTP SDK backend
7//! to communicate with runtara-core.
8
9use std::net::SocketAddr;
10use std::sync::Arc;
11
12use axum::extract::DefaultBodyLimit;
13use axum::{
14    Router,
15    extract::{Path, State},
16    http::StatusCode,
17    response::{IntoResponse, Json},
18    routing::{get, post},
19};
20use base64::Engine;
21use serde::{Deserialize, Serialize};
22use serde_json::{Value, json};
23use tracing::{error, info, warn};
24
25use crate::instance_handlers::{
26    self, CheckpointRequest as HandlerCheckpointRequest,
27    GetInstanceStatusRequest as HandlerGetStatusRequest, InstanceEvent as HandlerInstanceEvent,
28    InstanceEventType as HandlerEventType, InstanceHandlerState, InstanceStatus,
29    PollSignalsRequest as HandlerPollSignalsRequest,
30    RegisterInstanceRequest as HandlerRegisterRequest,
31    RetryAttemptEvent as HandlerRetryAttemptEvent, SignalAck as HandlerSignalAck, SignalType,
32    SleepRequest as HandlerSleepRequest,
33};
34
35// ============================================================================
36// JSON request/response types (mirror the protobuf types)
37// ============================================================================
38
39/// Register instance request
40#[derive(Debug, Deserialize)]
41pub struct RegisterRequest {
42    /// Tenant ID
43    pub tenant_id: String,
44    /// Optional checkpoint ID to resume from
45    #[serde(default)]
46    pub checkpoint_id: Option<String>,
47}
48
49/// Register instance response
50#[derive(Debug, Serialize)]
51pub struct RegisterResponse {
52    pub success: bool,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub error: Option<String>,
55}
56
57/// Checkpoint request
58#[derive(Debug, Deserialize)]
59pub struct CheckpointRequest {
60    /// Checkpoint identifier (unique per durable function call)
61    pub checkpoint_id: String,
62    /// Serialized workflow state (base64-encoded)
63    pub state: String,
64}
65
66/// Checkpoint response
67#[derive(Debug, Serialize)]
68pub struct CheckpointResponse {
69    /// True if a checkpoint with this ID already existed (resume case)
70    pub found: bool,
71    /// Existing checkpoint state (base64-encoded, present when found=true)
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub state: Option<String>,
74    /// Pending instance-wide signal (cancel/pause)
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub signal: Option<SignalInfo>,
77    /// Pending custom signal (WaitForSignal)
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub custom_signal: Option<CustomSignalInfo>,
80    /// Last error from a previous checkpoint attempt
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub last_error: Option<ErrorInfo>,
83}
84
85/// Signal information
86#[derive(Debug, Serialize)]
87pub struct SignalInfo {
88    pub signal_type: String,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub payload: Option<String>,
91}
92
93/// Custom signal information
94#[derive(Debug, Serialize)]
95pub struct CustomSignalInfo {
96    pub checkpoint_id: String,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub payload: Option<String>,
99}
100
101/// Error information
102#[derive(Debug, Serialize)]
103pub struct ErrorInfo {
104    pub code: String,
105    pub message: String,
106}
107
108/// Poll signals response
109#[derive(Debug, Serialize)]
110pub struct PollSignalsResponse {
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub signal: Option<SignalInfo>,
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub custom_signal: Option<CustomSignalInfo>,
115}
116
117/// Instance event request
118#[derive(Debug, Deserialize)]
119pub struct InstanceEventRequest {
120    /// Event type: "completed", "failed", "suspended", "custom"
121    pub event_type: String,
122    #[serde(default)]
123    pub checkpoint_id: Option<String>,
124    /// Payload (base64-encoded)
125    #[serde(default)]
126    pub payload: Option<String>,
127    #[serde(default)]
128    pub subtype: Option<String>,
129}
130
131/// Sleep request
132#[derive(Debug, Deserialize)]
133pub struct SleepRequest {
134    pub duration_ms: u64,
135    pub checkpoint_id: String,
136    /// Serialized state (base64-encoded)
137    pub state: String,
138}
139
140/// Signal acknowledgement request
141#[derive(Debug, Deserialize)]
142pub struct SignalAckRequest {
143    pub signal_type: String,
144}
145
146/// Retry attempt event
147#[derive(Debug, Deserialize)]
148pub struct RetryAttemptRequest {
149    pub checkpoint_id: String,
150    pub attempt: u32,
151    #[serde(default)]
152    pub error_message: Option<String>,
153}
154
155/// Generic success response
156#[derive(Debug, Serialize)]
157pub struct SuccessResponse {
158    pub success: bool,
159}
160
161// ============================================================================
162// Helper: convert proto signal types
163// ============================================================================
164
165fn signal_type_to_string(st: i32) -> String {
166    match st {
167        0 => "cancel".to_string(), // SignalCancel
168        1 => "pause".to_string(),  // SignalPause
169        2 => "resume".to_string(), // SignalResume
170        _ => format!("unknown({})", st),
171    }
172}
173
174fn event_type_from_string(s: &str) -> i32 {
175    match s {
176        "heartbeat" => HandlerEventType::EventHeartbeat as i32,
177        "completed" => HandlerEventType::EventCompleted as i32,
178        "failed" => HandlerEventType::EventFailed as i32,
179        "suspended" => HandlerEventType::EventSuspended as i32,
180        "custom" => HandlerEventType::EventCustom as i32,
181        _ => HandlerEventType::EventCustom as i32,
182    }
183}
184
185// ============================================================================
186// HTTP handlers
187// ============================================================================
188
189/// POST /api/v1/instances/{instance_id}/register
190async fn register_handler(
191    State(state): State<Arc<InstanceHandlerState>>,
192    Path(instance_id): Path<String>,
193    Json(body): Json<RegisterRequest>,
194) -> impl IntoResponse {
195    let request = HandlerRegisterRequest {
196        instance_id,
197        tenant_id: body.tenant_id,
198        checkpoint_id: body.checkpoint_id,
199    };
200
201    match instance_handlers::handle_register_instance(&state, request).await {
202        Ok(resp) => {
203            if resp.success {
204                Json(RegisterResponse {
205                    success: true,
206                    error: None,
207                })
208                .into_response()
209            } else {
210                (
211                    StatusCode::BAD_REQUEST,
212                    Json(RegisterResponse {
213                        success: false,
214                        error: Some(resp.error),
215                    }),
216                )
217                    .into_response()
218            }
219        }
220        Err(e) => {
221            error!("Register handler error: {}", e);
222            (
223                StatusCode::INTERNAL_SERVER_ERROR,
224                Json(json!({
225                    "error": e.to_string(),
226                    "code": "REGISTER_ERROR"
227                })),
228            )
229                .into_response()
230        }
231    }
232}
233
234/// POST /api/v1/instances/{instance_id}/checkpoint
235async fn checkpoint_handler(
236    State(state): State<Arc<InstanceHandlerState>>,
237    Path(instance_id): Path<String>,
238    Json(body): Json<CheckpointRequest>,
239) -> impl IntoResponse {
240    use base64::Engine;
241
242    let state_bytes = match base64::engine::general_purpose::STANDARD.decode(&body.state) {
243        Ok(b) => b,
244        Err(e) => {
245            return (
246                StatusCode::BAD_REQUEST,
247                Json(json!({
248                    "error": format!("Invalid base64 state: {}", e),
249                    "code": "INVALID_STATE"
250                })),
251            )
252                .into_response();
253        }
254    };
255
256    let request = HandlerCheckpointRequest {
257        instance_id,
258        checkpoint_id: body.checkpoint_id,
259        state: state_bytes,
260    };
261
262    match instance_handlers::handle_checkpoint(&state, request).await {
263        Ok(resp) => {
264            let signal = resp.pending_signal.map(|s| SignalInfo {
265                signal_type: signal_type_to_string(s.signal_type),
266                payload: if s.payload.is_empty() {
267                    None
268                } else {
269                    Some(base64::engine::general_purpose::STANDARD.encode(&s.payload))
270                },
271            });
272
273            let custom_signal = resp.custom_signal.map(|cs| CustomSignalInfo {
274                checkpoint_id: cs.checkpoint_id,
275                payload: if cs.payload.is_empty() {
276                    None
277                } else {
278                    Some(base64::engine::general_purpose::STANDARD.encode(&cs.payload))
279                },
280            });
281
282            let last_error = resp.last_error.map(|e| ErrorInfo {
283                code: e.code,
284                message: e.message,
285            });
286
287            Json(CheckpointResponse {
288                found: resp.found,
289                state: if resp.state.is_empty() {
290                    None
291                } else {
292                    Some(base64::engine::general_purpose::STANDARD.encode(&resp.state))
293                },
294                signal,
295                custom_signal,
296                last_error,
297            })
298            .into_response()
299        }
300        Err(e) => {
301            error!("Checkpoint handler error: {}", e);
302            (
303                StatusCode::INTERNAL_SERVER_ERROR,
304                Json(json!({
305                    "error": e.to_string(),
306                    "code": "CHECKPOINT_ERROR"
307                })),
308            )
309                .into_response()
310        }
311    }
312}
313
314/// GET /api/v1/instances/{instance_id}/signals
315async fn poll_signals_handler(
316    State(state): State<Arc<InstanceHandlerState>>,
317    Path(instance_id): Path<String>,
318) -> impl IntoResponse {
319    let request = HandlerPollSignalsRequest {
320        instance_id,
321        checkpoint_id: None,
322    };
323
324    match instance_handlers::handle_poll_signals(&state, request).await {
325        Ok(resp) => {
326            let signal = resp.signal.map(|s| SignalInfo {
327                signal_type: signal_type_to_string(s.signal_type),
328                payload: if s.payload.is_empty() {
329                    None
330                } else {
331                    Some(base64::engine::general_purpose::STANDARD.encode(&s.payload))
332                },
333            });
334
335            let custom_signal = resp.custom_signal.map(|cs| CustomSignalInfo {
336                checkpoint_id: cs.checkpoint_id,
337                payload: if cs.payload.is_empty() {
338                    None
339                } else {
340                    Some(base64::engine::general_purpose::STANDARD.encode(&cs.payload))
341                },
342            });
343
344            Json(PollSignalsResponse {
345                signal,
346                custom_signal,
347            })
348            .into_response()
349        }
350        Err(e) => {
351            error!("Poll signals error: {}", e);
352            (
353                StatusCode::INTERNAL_SERVER_ERROR,
354                Json(json!({
355                    "error": e.to_string(),
356                    "code": "POLL_SIGNALS_ERROR"
357                })),
358            )
359                .into_response()
360        }
361    }
362}
363
364/// GET /api/v1/instances/{instance_id}/signals/{signal_id}
365async fn poll_custom_signal_handler(
366    State(state): State<Arc<InstanceHandlerState>>,
367    Path((instance_id, signal_id)): Path<(String, String)>,
368) -> impl IntoResponse {
369    let request = HandlerPollSignalsRequest {
370        instance_id,
371        checkpoint_id: Some(signal_id),
372    };
373
374    match instance_handlers::handle_poll_signals(&state, request).await {
375        Ok(resp) => {
376            let custom_signal = resp.custom_signal.map(|cs| CustomSignalInfo {
377                checkpoint_id: cs.checkpoint_id,
378                payload: if cs.payload.is_empty() {
379                    None
380                } else {
381                    Some(base64::engine::general_purpose::STANDARD.encode(&cs.payload))
382                },
383            });
384
385            Json(PollSignalsResponse {
386                signal: None,
387                custom_signal,
388            })
389            .into_response()
390        }
391        Err(e) => {
392            error!("Poll custom signal error: {}", e);
393            (
394                StatusCode::INTERNAL_SERVER_ERROR,
395                Json(json!({
396                    "error": e.to_string(),
397                    "code": "POLL_CUSTOM_SIGNAL_ERROR"
398                })),
399            )
400                .into_response()
401        }
402    }
403}
404
405/// POST /api/v1/instances/{instance_id}/events
406async fn instance_event_handler(
407    State(state): State<Arc<InstanceHandlerState>>,
408    Path(instance_id): Path<String>,
409    Json(body): Json<InstanceEventRequest>,
410) -> impl IntoResponse {
411    let payload = body
412        .payload
413        .as_deref()
414        .map(|p| base64::engine::general_purpose::STANDARD.decode(p))
415        .transpose();
416
417    let payload = match payload {
418        Ok(p) => p.unwrap_or_default(),
419        Err(e) => {
420            return (
421                StatusCode::BAD_REQUEST,
422                Json(json!({
423                    "error": format!("Invalid base64 payload: {}", e),
424                    "code": "INVALID_PAYLOAD"
425                })),
426            )
427                .into_response();
428        }
429    };
430
431    let event = HandlerInstanceEvent {
432        instance_id,
433        event_type: event_type_from_string(&body.event_type),
434        checkpoint_id: body.checkpoint_id,
435        payload,
436        timestamp_ms: chrono::Utc::now().timestamp_millis(),
437        subtype: body.subtype,
438    };
439
440    match instance_handlers::handle_instance_event(&state, event).await {
441        Ok(resp) => {
442            if resp.success {
443                Json(SuccessResponse { success: true }).into_response()
444            } else {
445                let error = resp.error.unwrap_or_else(|| "Unknown error".to_string());
446                (
447                    StatusCode::INTERNAL_SERVER_ERROR,
448                    Json(json!({
449                        "success": false,
450                        "error": error,
451                    })),
452                )
453                    .into_response()
454            }
455        }
456        Err(e) => {
457            error!("Instance event error: {}", e);
458            (
459                StatusCode::INTERNAL_SERVER_ERROR,
460                Json(json!({
461                    "error": e.to_string(),
462                    "code": "EVENT_ERROR"
463                })),
464            )
465                .into_response()
466        }
467    }
468}
469
470/// POST /api/v1/instances/{instance_id}/completed
471async fn completed_handler(
472    State(state): State<Arc<InstanceHandlerState>>,
473    Path(instance_id): Path<String>,
474    Json(body): Json<Value>,
475) -> impl IntoResponse {
476    let payload = body
477        .get("output")
478        .and_then(|v| v.as_str())
479        .map(|s| base64::engine::general_purpose::STANDARD.decode(s))
480        .transpose();
481
482    let payload = match payload {
483        Ok(p) => p.unwrap_or_default(),
484        Err(e) => {
485            return (
486                StatusCode::BAD_REQUEST,
487                Json(json!({
488                    "error": format!("Invalid base64 output: {}", e),
489                    "code": "INVALID_OUTPUT"
490                })),
491            )
492                .into_response();
493        }
494    };
495
496    let event = HandlerInstanceEvent {
497        instance_id,
498        event_type: HandlerEventType::EventCompleted as i32,
499        checkpoint_id: None,
500        payload,
501        timestamp_ms: chrono::Utc::now().timestamp_millis(),
502        subtype: None,
503    };
504
505    match instance_handlers::handle_instance_event(&state, event).await {
506        Ok(_) => Json(SuccessResponse { success: true }).into_response(),
507        Err(e) => {
508            error!("Completed handler error: {}", e);
509            (
510                StatusCode::INTERNAL_SERVER_ERROR,
511                Json(json!({
512                    "error": e.to_string(),
513                    "code": "COMPLETED_ERROR"
514                })),
515            )
516                .into_response()
517        }
518    }
519}
520
521/// POST /api/v1/instances/{instance_id}/failed
522async fn failed_handler(
523    State(state): State<Arc<InstanceHandlerState>>,
524    Path(instance_id): Path<String>,
525    Json(body): Json<Value>,
526) -> impl IntoResponse {
527    let error_msg = body
528        .get("error")
529        .and_then(|v| v.as_str())
530        .unwrap_or("Unknown error");
531
532    let event = HandlerInstanceEvent {
533        instance_id,
534        event_type: HandlerEventType::EventFailed as i32,
535        checkpoint_id: None,
536        payload: error_msg.as_bytes().to_vec(),
537        timestamp_ms: chrono::Utc::now().timestamp_millis(),
538        subtype: None,
539    };
540
541    match instance_handlers::handle_instance_event(&state, event).await {
542        Ok(_) => Json(SuccessResponse { success: true }).into_response(),
543        Err(e) => {
544            error!("Failed handler error: {}", e);
545            (
546                StatusCode::INTERNAL_SERVER_ERROR,
547                Json(json!({
548                    "error": e.to_string(),
549                    "code": "FAILED_ERROR"
550                })),
551            )
552                .into_response()
553        }
554    }
555}
556
557/// POST /api/v1/instances/{instance_id}/suspended
558async fn suspended_handler(
559    State(state): State<Arc<InstanceHandlerState>>,
560    Path(instance_id): Path<String>,
561) -> impl IntoResponse {
562    let event = HandlerInstanceEvent {
563        instance_id,
564        event_type: HandlerEventType::EventSuspended as i32,
565        checkpoint_id: None,
566        payload: Vec::new(),
567        timestamp_ms: chrono::Utc::now().timestamp_millis(),
568        subtype: None,
569    };
570
571    match instance_handlers::handle_instance_event(&state, event).await {
572        Ok(_) => Json(SuccessResponse { success: true }).into_response(),
573        Err(e) => {
574            error!("Suspended handler error: {}", e);
575            (
576                StatusCode::INTERNAL_SERVER_ERROR,
577                Json(json!({
578                    "error": e.to_string(),
579                    "code": "SUSPENDED_ERROR"
580                })),
581            )
582                .into_response()
583        }
584    }
585}
586
587/// POST /api/v1/instances/{instance_id}/sleep
588async fn sleep_handler(
589    State(state): State<Arc<InstanceHandlerState>>,
590    Path(instance_id): Path<String>,
591    Json(body): Json<SleepRequest>,
592) -> impl IntoResponse {
593    let state_bytes = match base64::engine::general_purpose::STANDARD.decode(&body.state) {
594        Ok(b) => b,
595        Err(e) => {
596            return (
597                StatusCode::BAD_REQUEST,
598                Json(json!({
599                    "error": format!("Invalid base64 state: {}", e),
600                    "code": "INVALID_STATE"
601                })),
602            )
603                .into_response();
604        }
605    };
606
607    let request = HandlerSleepRequest {
608        instance_id,
609        duration_ms: body.duration_ms,
610        checkpoint_id: body.checkpoint_id,
611        state: state_bytes,
612    };
613
614    match instance_handlers::handle_sleep(&state, request).await {
615        Ok(_) => Json(SuccessResponse { success: true }).into_response(),
616        Err(e) => {
617            error!("Sleep handler error: {}", e);
618            (
619                StatusCode::INTERNAL_SERVER_ERROR,
620                Json(json!({
621                    "error": e.to_string(),
622                    "code": "SLEEP_ERROR"
623                })),
624            )
625                .into_response()
626        }
627    }
628}
629
630/// POST /api/v1/instances/{instance_id}/signals/ack
631async fn signal_ack_handler(
632    State(state): State<Arc<InstanceHandlerState>>,
633    Path(instance_id): Path<String>,
634    Json(body): Json<SignalAckRequest>,
635) -> impl IntoResponse {
636    let signal_type = match body.signal_type.as_str() {
637        "cancel" => SignalType::SignalCancel as i32,
638        "pause" => SignalType::SignalPause as i32,
639        "resume" => SignalType::SignalResume as i32,
640        _ => {
641            return (
642                StatusCode::BAD_REQUEST,
643                Json(json!({
644                    "error": format!("Unknown signal type: {}", body.signal_type),
645                    "code": "INVALID_SIGNAL_TYPE"
646                })),
647            )
648                .into_response();
649        }
650    };
651
652    let ack = HandlerSignalAck {
653        instance_id,
654        signal_type,
655        acknowledged: true,
656    };
657
658    match instance_handlers::handle_signal_ack(&state, ack).await {
659        Ok(()) => Json(SuccessResponse { success: true }).into_response(),
660        Err(e) => {
661            warn!("Signal ack error: {}", e);
662            (
663                StatusCode::INTERNAL_SERVER_ERROR,
664                Json(json!({
665                    "error": e.to_string(),
666                    "code": "SIGNAL_ACK_ERROR"
667                })),
668            )
669                .into_response()
670        }
671    }
672}
673
674/// POST /api/v1/instances/{instance_id}/retry
675async fn retry_handler(
676    State(state): State<Arc<InstanceHandlerState>>,
677    Path(instance_id): Path<String>,
678    Json(body): Json<RetryAttemptRequest>,
679) -> impl IntoResponse {
680    let event = HandlerRetryAttemptEvent {
681        instance_id,
682        checkpoint_id: body.checkpoint_id,
683        attempt_number: body.attempt,
684        error_message: body.error_message,
685        error_metadata: None,
686        timestamp_ms: chrono::Utc::now().timestamp_millis(),
687    };
688
689    match instance_handlers::handle_retry_attempt(&state, event).await {
690        Ok(()) => Json(SuccessResponse { success: true }).into_response(),
691        Err(e) => {
692            warn!("Retry attempt error: {}", e);
693            (
694                StatusCode::INTERNAL_SERVER_ERROR,
695                Json(json!({
696                    "error": e.to_string(),
697                    "code": "RETRY_ERROR"
698                })),
699            )
700                .into_response()
701        }
702    }
703}
704
705/// Instance status response
706#[derive(Debug, Serialize)]
707pub struct InstanceStatusResponse {
708    pub found: bool,
709    pub instance_id: String,
710    #[serde(skip_serializing_if = "Option::is_none")]
711    pub status: Option<String>,
712    #[serde(skip_serializing_if = "Option::is_none")]
713    pub checkpoint_id: Option<String>,
714    #[serde(skip_serializing_if = "Option::is_none")]
715    pub output: Option<String>, // base64
716    #[serde(skip_serializing_if = "Option::is_none")]
717    pub error: Option<String>,
718}
719
720/// GET /api/v1/instances/{instance_id}/status
721async fn status_handler(
722    State(state): State<Arc<InstanceHandlerState>>,
723    Path(instance_id): Path<String>,
724) -> impl IntoResponse {
725    let request = HandlerGetStatusRequest {
726        instance_id: instance_id.clone(),
727    };
728
729    match instance_handlers::handle_get_instance_status(&state, request).await {
730        Ok(resp) => {
731            let status_str = match InstanceStatus::try_from_i32(resp.status) {
732                Some(InstanceStatus::StatusPending) => "pending",
733                Some(InstanceStatus::StatusRunning) => "running",
734                Some(InstanceStatus::StatusSuspended) => "suspended",
735                Some(InstanceStatus::StatusCompleted) => "completed",
736                Some(InstanceStatus::StatusFailed) => "failed",
737                Some(InstanceStatus::StatusCancelled) => "cancelled",
738                _ => "unknown",
739            };
740
741            let output = resp
742                .output
743                .as_ref()
744                .map(|o| base64::engine::general_purpose::STANDARD.encode(o));
745
746            let found =
747                status_str != "unknown" || resp.error.as_deref() != Some("Instance not found");
748
749            Json(InstanceStatusResponse {
750                found,
751                instance_id: resp.instance_id,
752                status: Some(status_str.to_string()),
753                checkpoint_id: resp.checkpoint_id,
754                output,
755                error: resp.error,
756            })
757            .into_response()
758        }
759        Err(e) => {
760            error!("Status handler error: {}", e);
761            (
762                StatusCode::INTERNAL_SERVER_ERROR,
763                Json(json!({
764                    "error": e.to_string(),
765                    "code": "STATUS_ERROR"
766                })),
767            )
768                .into_response()
769        }
770    }
771}
772
773/// GET /api/v1/instances/{instance_id}/input
774async fn input_handler(
775    State(state): State<Arc<InstanceHandlerState>>,
776    Path(instance_id): Path<String>,
777) -> impl IntoResponse {
778    match state.persistence.get_instance(&instance_id).await {
779        Ok(Some(inst)) => {
780            if let Some(input_bytes) = inst.input {
781                let encoded = base64::engine::general_purpose::STANDARD.encode(&input_bytes);
782                Json(json!({
783                    "found": true,
784                    "instance_id": instance_id,
785                    "input": encoded,
786                }))
787                .into_response()
788            } else {
789                Json(json!({
790                    "found": true,
791                    "instance_id": instance_id,
792                    "input": null,
793                }))
794                .into_response()
795            }
796        }
797        Ok(None) => (
798            StatusCode::NOT_FOUND,
799            Json(json!({
800                "found": false,
801                "instance_id": instance_id,
802                "error": "Instance not found",
803            })),
804        )
805            .into_response(),
806        Err(e) => {
807            error!("Input handler error: {}", e);
808            (
809                StatusCode::INTERNAL_SERVER_ERROR,
810                Json(json!({
811                    "error": e.to_string(),
812                    "code": "INPUT_ERROR"
813                })),
814            )
815                .into_response()
816        }
817    }
818}
819
820/// GET /health
821async fn health_handler(State(state): State<Arc<InstanceHandlerState>>) -> impl IntoResponse {
822    let db_ok = state.persistence.health_check_db().await.unwrap_or(false);
823    if db_ok {
824        Json(json!({"status": "healthy"})).into_response()
825    } else {
826        (
827            StatusCode::SERVICE_UNAVAILABLE,
828            Json(json!({
829                "status": "unhealthy",
830                "error": "database check failed"
831            })),
832        )
833            .into_response()
834    }
835}
836
837// ============================================================================
838// Router and server
839// ============================================================================
840
841/// Build the instance protocol HTTP router.
842///
843/// All routes are prefixed with `/api/v1`.
844pub fn instance_http_router(state: Arc<InstanceHandlerState>) -> Router {
845    Router::new()
846        // Instance lifecycle
847        .route(
848            "/api/v1/instances/{instance_id}/register",
849            post(register_handler),
850        )
851        // Checkpointing
852        .route(
853            "/api/v1/instances/{instance_id}/checkpoint",
854            post(checkpoint_handler),
855        )
856        // Signal polling
857        .route(
858            "/api/v1/instances/{instance_id}/signals",
859            get(poll_signals_handler),
860        )
861        .route(
862            "/api/v1/instances/{instance_id}/signals/{signal_id}",
863            get(poll_custom_signal_handler),
864        )
865        .route(
866            "/api/v1/instances/{instance_id}/signals/ack",
867            post(signal_ack_handler),
868        )
869        // Instance events (completion, failure, suspension)
870        .route(
871            "/api/v1/instances/{instance_id}/completed",
872            post(completed_handler),
873        )
874        .route(
875            "/api/v1/instances/{instance_id}/failed",
876            post(failed_handler),
877        )
878        .route(
879            "/api/v1/instances/{instance_id}/suspended",
880            post(suspended_handler),
881        )
882        .route(
883            "/api/v1/instances/{instance_id}/events",
884            post(instance_event_handler),
885        )
886        // Sleep/wake
887        .route("/api/v1/instances/{instance_id}/sleep", post(sleep_handler))
888        // Retry tracking
889        .route("/api/v1/instances/{instance_id}/retry", post(retry_handler))
890        // Instance status
891        .route(
892            "/api/v1/instances/{instance_id}/status",
893            get(status_handler),
894        )
895        // Instance input
896        .route("/api/v1/instances/{instance_id}/input", get(input_handler))
897        // Health check
898        .route("/health", get(health_handler))
899        .layer(DefaultBodyLimit::max(64 * 1024 * 1024))
900        .with_state(state)
901}
902
903/// Run the instance HTTP server.
904///
905/// Starts an axum HTTP server on the given address, serving the instance
906/// protocol API for all clients (native scenarios, WASM scenarios, debugging, etc.).
907pub async fn run_http_server(
908    bind_addr: SocketAddr,
909    state: Arc<InstanceHandlerState>,
910) -> anyhow::Result<()> {
911    let app = instance_http_router(state);
912    let listener = tokio::net::TcpListener::bind(bind_addr).await?;
913
914    info!(addr = %bind_addr, "Instance HTTP server starting");
915
916    axum::serve(listener, app)
917        .await
918        .map_err(|e| anyhow::anyhow!("HTTP server error: {}", e))?;
919
920    info!("Instance HTTP server stopped");
921    Ok(())
922}