Skip to main content

futu_rest/adapter/
proto_request.rs

1//! Split from adapter.rs: proto_request.
2//!
3//! pub items: proto_request,proto_request_with_filter,proto_request_with_idempotency,proto_request_with_idempotency_and_caller,proto_request_with_ctx.
4
5use axum::Json;
6use axum::http::StatusCode;
7use axum::http::header::CONTENT_TYPE;
8use axum::response::{IntoResponse, Response};
9use serde_json::Value;
10
11use super::*;
12
13mod decode;
14pub(crate) use decode::{
15    JsonRequestMode, decode_json_request, decode_json_request_with_surface_spec,
16};
17
18#[derive(Debug, Clone)]
19pub struct RawJson {
20    bytes: Bytes,
21}
22
23impl RawJson {
24    pub fn new(bytes: impl Into<Bytes>) -> Self {
25        Self {
26            bytes: bytes.into(),
27        }
28    }
29}
30
31impl IntoResponse for RawJson {
32    fn into_response(self) -> Response {
33        ([(CONTENT_TYPE, "application/json")], self.bytes).into_response()
34    }
35}
36
37///
38/// 泛型参数:
39/// - `Req`: protobuf 请求类型 (prost::Message + serde::Deserialize)
40/// - `Rsp`: protobuf 响应类型 (prost::Message + serde::Serialize)
41///
42/// 流程: JSON → Req → encode → dispatch(proto_id) → decode → Rsp → JSON
43pub async fn proto_request<Req, Rsp>(
44    state: &RestState,
45    proto_id: u32,
46    json_body: Option<Value>,
47) -> Result<Json<Value>, (StatusCode, Json<Value>)>
48where
49    Req: Message + Default + serde::de::DeserializeOwned,
50    Rsp: Message + Default + serde::Serialize,
51{
52    proto_request_internal::<Req, Rsp>(state, proto_id, json_body, None, None, None).await
53}
54
55pub(crate) async fn proto_request_with_surface_spec_and_idempotency<Req, Rsp>(
56    state: &RestState,
57    proto_id: u32,
58    json_body: Option<Value>,
59    idempotency_key: Option<String>,
60    surface_spec: &'static EndpointSpec,
61) -> Result<Json<Value>, (StatusCode, Json<Value>)>
62where
63    Req: Message + Default + serde::de::DeserializeOwned,
64    Rsp: Message + Default + serde::Serialize,
65{
66    proto_request_internal::<Req, Rsp>(
67        state,
68        proto_id,
69        json_body,
70        idempotency_key,
71        None,
72        Some(surface_spec),
73    )
74    .await
75}
76
77pub async fn proto_request_raw<Req, Rsp>(
78    state: &RestState,
79    proto_id: u32,
80    json_body: Option<Value>,
81) -> Result<RawJson, (StatusCode, Json<Value>)>
82where
83    Req: Message + Default + serde::de::DeserializeOwned,
84    Rsp: Message + Default + serde::Serialize,
85{
86    proto_request_raw_internal::<Req, Rsp>(
87        state,
88        proto_id,
89        json_body,
90        None,
91        None,
92        JsonRequestMode::GenericRest,
93    )
94    .await
95}
96
97pub async fn proto_request_raw_spec_body<Req, Rsp>(
98    state: &RestState,
99    proto_id: u32,
100    json_body: Option<Value>,
101) -> Result<RawJson, (StatusCode, Json<Value>)>
102where
103    Req: Message + Default + serde::de::DeserializeOwned,
104    Rsp: Message + Default + serde::Serialize,
105{
106    proto_request_raw_internal::<Req, Rsp>(
107        state,
108        proto_id,
109        json_body,
110        None,
111        None,
112        JsonRequestMode::RawSpecBody,
113    )
114    .await
115}
116
117/// 把 `DispatchError` 翻译成 REST 400 response (HTTP + JSON body).
118///
119/// pitfall #45 loud error: 不返 silent empty ret_type=0, 返清晰 400 + ret_msg.
120pub(super) fn map_dispatch_error(
121    spec: &'static EndpointSpec,
122    err: DispatchError,
123) -> (StatusCode, Json<Value>) {
124    let proto_id = spec
125        .proto_id()
126        .map(|id| id.to_string())
127        .unwrap_or_else(|| "daemon-local".to_string());
128    let ret_msg = format!(
129        "{} (endpoint: {}, proto_id: {})",
130        err, spec.canonical_name, proto_id
131    );
132    let status = StatusCode::from_u16(spec.runtime.error.validation_http_status)
133        .unwrap_or(StatusCode::BAD_REQUEST);
134    let machine_error_field = spec.runtime.error.machine_error_field;
135    let mut body = serde_json::json!({
136        "ret_type": -1,
137        "ret_msg": ret_msg,
138    });
139    if let Some(obj) = body.as_object_mut() {
140        obj.insert(
141            machine_error_field.to_string(),
142            serde_json::json!({
143                "kind": "validation_error",
144                "message": err.to_string(),
145                "endpoint": spec.canonical_name,
146                "proto_id": proto_id,
147            }),
148        );
149    }
150    (status, Json(body))
151}
152
153pub(super) fn validation_error_body(message: impl Into<String>) -> Value {
154    let message = message.into();
155    serde_json::json!({
156        "ret_type": -1,
157        "ret_msg": message,
158        "error": message,
159    })
160}
161
162/// v1.4.104 阶段 7-1: 走 FilterRegistry 的 proto_request 变体.
163///
164/// 跟 [`proto_request`] / [`proto_request_with_idempotency`] 流程一致, 但在
165/// `decode protobuf 响应` 之前**插入 FilterRegistry::apply** 一步, 按
166/// `allowed_acc_ids` filter 受限 key 的响应 acc_list (proto 2001 TRD_GET_ACC_LIST
167/// 等). 与 gRPC server.rs / WS ws_listener.rs 同源 (单一 registry).
168///
169/// `allowed_acc_ids = None` 时 filter no-op (legacy / 无限制 key).
170///
171/// **codex 0522 F2 v1.4.106**: 推荐改用 [`proto_request_with_ctx`], 这个
172/// helper 改为内部包装, 丢失 `caller_key_id`. 保留作 backward-compat 给
173/// 已有的 acc_list filter call site 不破坏 (route 层迁移到 ctx 后此函数
174/// 全部 caller 应该归零, 保留一段过渡期再删).
175pub async fn proto_request_with_filter<Req, Rsp>(
176    state: &RestState,
177    proto_id: u32,
178    json_body: Option<Value>,
179    allowed_acc_ids: Option<&std::collections::HashSet<u64>>,
180) -> Result<Json<Value>, (StatusCode, Json<Value>)>
181where
182    Req: Message + Default + serde::de::DeserializeOwned,
183    Rsp: Message + Default + serde::Serialize,
184{
185    // codex 0522 F1 v1.4.106: 构 minimal CallerContext 把 allowed_acc_ids 同时
186    // 接进 IncomingRequest.caller_allowed_acc_ids + FilterRegistry. caller_key_id
187    // 仍 None (老 call site 没传). 推荐 caller 迁移到 proto_request_with_ctx.
188    let ctx = if let Some(allowed) = allowed_acc_ids {
189        crate::caller_context::CallerContext {
190            key_id: None,
191            allowed_acc_ids: Some(std::sync::Arc::new(allowed.clone())),
192            has_auth_setup_scope: false,
193            is_loopback: false,
194            legacy_local_mode: false,
195        }
196    } else {
197        crate::caller_context::CallerContext::legacy()
198    };
199    proto_request_internal::<Req, Rsp>(state, proto_id, json_body, None, Some(&ctx), None).await
200}
201
202/// v1.4.38 Phase 4: 支持 `Idempotency-Key` header 的 proto_request。
203/// 老 call site 继续用 `proto_request`(header=None), 新写 trade endpoint
204/// 用 `proto_request_with_idempotency` 从 axum HeaderMap 提取 header 后传入。
205pub async fn proto_request_with_idempotency<Req, Rsp>(
206    state: &RestState,
207    proto_id: u32,
208    json_body: Option<Value>,
209    idempotency_key: Option<String>,
210) -> Result<Json<Value>, (StatusCode, Json<Value>)>
211where
212    Req: Message + Default + serde::de::DeserializeOwned,
213    Rsp: Message + Default + serde::Serialize,
214{
215    proto_request_internal::<Req, Rsp>(state, proto_id, json_body, idempotency_key, None, None)
216        .await
217}
218
219/// v1.4.106 codex 0920 F1 (P1): 支持 caller key id 的 idempotency 变体.
220/// 让 cache namespace 跨 caller 隔离 — 不同 caller 用同 Idempotency-Key
221/// **不能** 跨 caller 命中老 response (避免跨账户数据泄漏 + 重复下单).
222///
223/// **call site**: REST trade endpoint (place / modify / cancel / reconfirm)
224/// 在 `rec: Option<Extension<Arc<KeyRecord>>>` 抽 caller_key_id 后传入.
225pub async fn proto_request_with_idempotency_and_caller<Req, Rsp>(
226    state: &RestState,
227    proto_id: u32,
228    json_body: Option<Value>,
229    idempotency_key: Option<String>,
230    caller_key_id: Option<String>,
231) -> Result<Json<Value>, (StatusCode, Json<Value>)>
232where
233    Req: Message + Default + serde::de::DeserializeOwned,
234    Rsp: Message + Default + serde::Serialize,
235{
236    let ctx = caller_key_id.map(|k| crate::caller_context::CallerContext {
237        key_id: Some(k),
238        allowed_acc_ids: None,
239        has_auth_setup_scope: false,
240        is_loopback: false,
241        legacy_local_mode: false,
242    });
243    proto_request_internal::<Req, Rsp>(
244        state,
245        proto_id,
246        json_body,
247        idempotency_key,
248        ctx.as_ref(),
249        None,
250    )
251    .await
252}
253
254/// codex 0522 F1 v1.4.106 (推荐 API): 带 `CallerContext` 的 proto_request.
255///
256/// 与 `proto_request_with_idempotency` / `proto_request_with_filter` 的关系:
257/// 后两者只接 `idempotency_key` 或 `allowed_acc_ids` 单一维度, 本函数接完整
258/// `CallerContext` (含 `key_id` + `allowed_acc_ids`), 把 caller scope **同时**
259/// 接到三个下游消费点:
260///
261/// 1. `IncomingRequest.caller_allowed_acc_ids` (dispatch handler 的 per-acc
262///    enforce, defense-in-depth)
263/// 2. `IncomingRequest.caller_key_id` (per-key 配额 / cleanup / 审计入口)
264/// 3. `FilterRegistry::apply` (响应 acc_list 过滤)
265///
266/// 任一接错 → `dispatch handler` 看到 `None` 就 silent bypass (codex F1 audit
267/// 实锤的 v1.4.105 之前 REST regression).
268///
269/// `ctx = None` 等价 `legacy mode` (无 caller key, 无 acc 限制) — 通常仅
270/// 测试 / 显式 unauthenticated route 用. 真 route handler 应该总是构 ctx
271/// 从 `Extension<Arc<KeyRecord>>` 抽出来 (`CallerContext::from_key_record`).
272pub async fn proto_request_with_ctx<Req, Rsp>(
273    state: &RestState,
274    proto_id: u32,
275    json_body: Option<Value>,
276    idempotency_key: Option<String>,
277    ctx: Option<&crate::caller_context::CallerContext>,
278) -> Result<Json<Value>, (StatusCode, Json<Value>)>
279where
280    Req: Message + Default + serde::de::DeserializeOwned,
281    Rsp: Message + Default + serde::Serialize,
282{
283    proto_request_internal::<Req, Rsp>(state, proto_id, json_body, idempotency_key, ctx, None).await
284}
285
286async fn proto_request_raw_internal<Req, Rsp>(
287    state: &RestState,
288    proto_id: u32,
289    json_body: Option<Value>,
290    idempotency_key: Option<String>,
291    ctx: Option<&crate::caller_context::CallerContext>,
292    mode: JsonRequestMode,
293) -> Result<RawJson, (StatusCode, Json<Value>)>
294where
295    Req: Message + Default + serde::de::DeserializeOwned,
296    Rsp: Message + Default + serde::Serialize,
297{
298    let req_msg: Req = decode_json_request(proto_id, json_body, mode)?;
299    let conn_id = state.next_conn_id();
300    let serial_no = state.next_serial();
301    let body = futu_server::trade_packet_id::fill_omitted_trade_packet_id_bytes(
302        proto_id,
303        req_msg.encode_to_vec(),
304        conn_id,
305        serial_no,
306    )
307    .map(Bytes::from)
308    .map_err(|e| {
309        (
310            StatusCode::INTERNAL_SERVER_ERROR,
311            Json(serde_json::json!({
312                "error": format!("failed to prepare trade PacketID: {e}")
313            })),
314        )
315    })?;
316
317    let incoming =
318        IncomingRequest::builder(conn_id, proto_id, serial_no, ProtoFmtType::Protobuf, body)
319            .with_idempotency_key(idempotency_key)
320            .with_caller_scope(
321                ctx.and_then(|c| c.caller_allowed_acc_ids_arc()),
322                ctx.and_then(|c| c.caller_key_id()),
323            )
324            .with_auth_setup_admission(
325                ctx.is_some_and(|c| c.caller_has_auth_setup_scope()),
326                ctx.is_some_and(|c| c.caller_is_loopback()),
327                ctx.is_some_and(|c| c.caller_legacy_local_mode()),
328            )
329            .build();
330
331    let resp_bytes = state
332        .router
333        .dispatch(incoming.conn_id, &incoming)
334        .await
335        .ok_or_else(|| {
336            (
337                StatusCode::INTERNAL_SERVER_ERROR,
338                Json(serde_json::json!({
339                    "error": "handler returned no response"
340                })),
341            )
342        })?;
343
344    let resp_bytes = state.filter_registry.apply(
345        proto_id,
346        resp_bytes,
347        ctx.and_then(|c| c.allowed_acc_ids_borrow()),
348    );
349
350    let rsp_msg = Rsp::decode(Bytes::from(resp_bytes)).map_err(|e| {
351        (
352            StatusCode::INTERNAL_SERVER_ERROR,
353            Json(serde_json::json!({
354                "error": format!("failed to decode response: {e}")
355            })),
356        )
357    })?;
358
359    raw_json_from_proto_response(&rsp_msg)
360}
361
362pub(crate) fn raw_json_from_proto_response<Rsp>(
363    rsp_msg: &Rsp,
364) -> Result<RawJson, (StatusCode, Json<Value>)>
365where
366    Rsp: serde::Serialize,
367{
368    let encoded = encode_proto_response_raw_or_error_value(rsp_msg).map_err(|e| {
369        (
370            StatusCode::INTERNAL_SERVER_ERROR,
371            Json(serde_json::json!({
372                "error": e
373            })),
374        )
375    })?;
376    match encoded {
377        ProtoJsonBody::Raw(raw) => Ok(RawJson::new(raw)),
378        ProtoJsonBody::Value(value) => {
379            let raw = serde_json::to_vec(&value).map_err(|e| {
380                (
381                    StatusCode::INTERNAL_SERVER_ERROR,
382                    Json(serde_json::json!({
383                        "error": format!("failed to serialize response: {e}")
384                    })),
385                )
386            })?;
387            Ok(RawJson::new(raw))
388        }
389    }
390}
391
392/// v1.4.104 阶段 7-1 + codex 0522 F1 v1.4.106: 内部统一实现, 接 `CallerContext`
393/// 替代单 `allowed_acc_ids` 入参. 调度时同时填 `IncomingRequest.caller_key_id`
394/// 与 `caller_allowed_acc_ids`, 响应过滤共用同一个 `allowed_acc_ids` 借引用 ——
395/// 单一来源, 防 routes 层手写多套 caller scope check 漂移 (codex F2).
396async fn proto_request_internal<Req, Rsp>(
397    state: &RestState,
398    proto_id: u32,
399    json_body: Option<Value>,
400    idempotency_key: Option<String>,
401    ctx: Option<&crate::caller_context::CallerContext>,
402    surface_spec: Option<&'static EndpointSpec>,
403) -> Result<Json<Value>, (StatusCode, Json<Value>)>
404where
405    Req: Message + Default + serde::de::DeserializeOwned,
406    Rsp: Message + Default + serde::Serialize,
407{
408    // 1. JSON → protobuf 请求. Empty body still flows through EndpointSpec
409    // validation; otherwise required fields could silently become
410    // `Req::default()` and bypass REST contract checks.
411    let req_msg: Req = decode_json_request_with_surface_spec(
412        proto_id,
413        json_body,
414        JsonRequestMode::GenericRest,
415        surface_spec,
416    )?;
417
418    // 2. encode 为 protobuf bytes
419    let conn_id = state.next_conn_id();
420    let serial_no = state.next_serial();
421    let body = futu_server::trade_packet_id::fill_omitted_trade_packet_id_bytes(
422        proto_id,
423        req_msg.encode_to_vec(),
424        conn_id,
425        serial_no,
426    )
427    .map(Bytes::from)
428    .map_err(|e| {
429        (
430            StatusCode::INTERNAL_SERVER_ERROR,
431            Json(serde_json::json!({
432                "error": format!("failed to prepare trade PacketID: {e}")
433            })),
434        )
435    })?;
436
437    // 3. 构造 IncomingRequest 调用现有 handler
438    // codex 0522 F1 v1.4.106: caller_allowed_acc_ids + caller_key_id 同时从
439    // CallerContext 拿, 单一来源, 不再分别写 None.
440    let incoming =
441        IncomingRequest::builder(conn_id, proto_id, serial_no, ProtoFmtType::Protobuf, body)
442            .with_idempotency_key(idempotency_key)
443            .with_caller_scope(
444                ctx.and_then(|c| c.caller_allowed_acc_ids_arc()),
445                ctx.and_then(|c| c.caller_key_id()),
446            )
447            .with_auth_setup_admission(
448                ctx.is_some_and(|c| c.caller_has_auth_setup_scope()),
449                ctx.is_some_and(|c| c.caller_is_loopback()),
450                ctx.is_some_and(|c| c.caller_legacy_local_mode()),
451            )
452            .build();
453
454    let resp_bytes = state
455        .router
456        .dispatch(incoming.conn_id, &incoming)
457        .await
458        .ok_or_else(|| {
459            (
460                StatusCode::INTERNAL_SERVER_ERROR,
461                Json(serde_json::json!({
462                    "error": "handler returned no response"
463                })),
464            )
465        })?;
466
467    // 3.5. v1.4.104 阶段 7-1: response filter (cross-surface 共享 FilterRegistry).
468    //      proto 2001 TRD_GET_ACC_LIST 默认注册, allowed_acc_ids 非空时 filter
469    //      响应 acc_list, 受限 key 不能跨账户 enumerate. legacy / 无限制 key /
470    //      非注册 proto_id → no-op 返原 bytes 不动.
471    // codex 0522 F2 v1.4.106: 同一 ctx.allowed_acc_ids 借引用 (与
472    // IncomingRequest.caller_allowed_acc_ids 是同一份 Arc), 单一来源.
473    let resp_bytes = state.filter_registry.apply(
474        proto_id,
475        resp_bytes,
476        ctx.and_then(|c| c.allowed_acc_ids_borrow()),
477    );
478
479    // 4. decode protobuf 响应
480    let rsp_msg = Rsp::decode(Bytes::from(resp_bytes)).map_err(|e| {
481        (
482            StatusCode::INTERNAL_SERVER_ERROR,
483            Json(serde_json::json!({
484                "error": format!("failed to decode response: {e}")
485            })),
486        )
487    })?;
488
489    // 5. 序列化为 JSON
490    let mut json_rsp = serde_json::to_value(&rsp_msg).map_err(|e| {
491        (
492            StatusCode::INTERNAL_SERVER_ERROR,
493            Json(serde_json::json!({
494                "error": format!("failed to serialize response: {e}")
495            })),
496        )
497    })?;
498
499    // 6. v1.4.34 BUG-2b 修:给所有错误响应的 ret_msg 加 `[err_code=X]` 前缀
500    //    历史:v1.4.27 server_err() helper 在 futu-trd 客户端 lib 里包了 CLI/gRPC/MCP
501    //    三个 surface,漏了 REST 这个 surface。external reviewer 4 次独立复现;v1.4.30 / 31 /
502    //    32 / 33 都没修。根因:REST 走 proto_request 直接序列化响应,没经过 server_err
503    //    helper 层。v1.4.34 直接在 JSON 层包一下,不动 daemon response(daemon 要给
504    //    CLI 客户端留原始 err_code 字段,CLI 自己会包)。
505    maybe_wrap_err_code_prefix(&mut json_rsp);
506
507    Ok(Json(json_rsp))
508}
509
510pub(super) enum ProtoJsonBody {
511    Raw(Bytes),
512    Value(Value),
513}
514
515pub(super) fn encode_proto_response_raw_or_error_value<Rsp>(
516    rsp_msg: &Rsp,
517) -> Result<ProtoJsonBody, String>
518where
519    Rsp: serde::Serialize,
520{
521    let raw =
522        serde_json::to_vec(rsp_msg).map_err(|e| format!("failed to serialize response: {e}"))?;
523    if serialized_ret_type(&raw) == Some(0) {
524        return Ok(ProtoJsonBody::Raw(Bytes::from(raw)));
525    }
526
527    let mut json_rsp =
528        serde_json::to_value(rsp_msg).map_err(|e| format!("failed to serialize response: {e}"))?;
529    maybe_wrap_err_code_prefix(&mut json_rsp);
530    Ok(ProtoJsonBody::Value(json_rsp))
531}
532
533fn serialized_ret_type(raw: &[u8]) -> Option<i64> {
534    #[derive(serde::Deserialize)]
535    struct RetOnly {
536        ret_type: Option<i64>,
537    }
538
539    serde_json::from_slice::<RetOnly>(raw)
540        .ok()
541        .and_then(|ret| ret.ret_type)
542}
543
544/// v1.4.34 BUG-2b:REST 响应的 ret_msg 包 `[err_code=X]` 前缀。
545///
546/// 与 `futu_trd::server_err()` 同语义,但作用在已序列化的 JSON 上:
547///
548/// - `ret_type == 0`:成功,不动
549/// - `ret_type != 0` 且 `err_code` 非 null:`[err_code=<code>] <原 ret_msg>`
550/// - `ret_type != 0` 且 `err_code` 缺省:`[err_code=none] <原 ret_msg>`
551/// - `ret_type != 0` 且 ret_msg 空:只留 `[err_code=<X>]` 带方括号的标签
552///
553/// 幂等(已经带 `[err_code=` 前缀的 ret_msg 不重复包)。方括号位置精确,既
554/// 便于客户端 grep,也不误伤"错误描述里恰好有方括号"的正常 msg。
555///
556/// 只对**顶层**的 `ret_type / ret_msg / err_code` 生效;嵌套在 s2c 里的状态字段
557/// 不动。
558pub(crate) fn maybe_wrap_err_code_prefix(v: &mut Value) {
559    let obj = match v.as_object_mut() {
560        Some(o) => o,
561        None => return,
562    };
563    // 只处理失败响应
564    let is_err = obj
565        .get("ret_type")
566        .and_then(|t| t.as_i64())
567        .map(|t| t != 0)
568        .unwrap_or(false);
569    if !is_err {
570        return;
571    }
572    // 读当前 msg(可能是 null)
573    let raw_msg = obj
574        .get("ret_msg")
575        .and_then(|m| m.as_str())
576        .unwrap_or("")
577        .to_string();
578    // 幂等:已带前缀就不动(多轮 middleware 不应该双包)
579    if raw_msg.starts_with("[err_code=") {
580        return;
581    }
582    // 读 err_code(可能是 null / 整数)
583    let err_code_label = match obj.get("err_code") {
584        Some(Value::Number(n)) => n
585            .as_i64()
586            .map(|i| i.to_string())
587            .unwrap_or("none".to_string()),
588        _ => "none".to_string(),
589    };
590    let new_msg = if raw_msg.is_empty() {
591        format!("[err_code={err_code_label}]")
592    } else {
593        format!("[err_code={err_code_label}] {raw_msg}")
594    };
595    obj.insert("ret_msg".to_string(), Value::String(new_msg));
596}