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        }
193    } else {
194        crate::caller_context::CallerContext::legacy()
195    };
196    proto_request_internal::<Req, Rsp>(state, proto_id, json_body, None, Some(&ctx), None).await
197}
198
199/// v1.4.38 Phase 4: 支持 `Idempotency-Key` header 的 proto_request。
200/// 老 call site 继续用 `proto_request`(header=None), 新写 trade endpoint
201/// 用 `proto_request_with_idempotency` 从 axum HeaderMap 提取 header 后传入。
202pub async fn proto_request_with_idempotency<Req, Rsp>(
203    state: &RestState,
204    proto_id: u32,
205    json_body: Option<Value>,
206    idempotency_key: Option<String>,
207) -> Result<Json<Value>, (StatusCode, Json<Value>)>
208where
209    Req: Message + Default + serde::de::DeserializeOwned,
210    Rsp: Message + Default + serde::Serialize,
211{
212    proto_request_internal::<Req, Rsp>(state, proto_id, json_body, idempotency_key, None, None)
213        .await
214}
215
216/// v1.4.106 codex 0920 F1 (P1): 支持 caller key id 的 idempotency 变体.
217/// 让 cache namespace 跨 caller 隔离 — 不同 caller 用同 Idempotency-Key
218/// **不能** 跨 caller 命中老 response (避免跨账户数据泄漏 + 重复下单).
219///
220/// **call site**: REST trade endpoint (place / modify / cancel / reconfirm)
221/// 在 `rec: Option<Extension<Arc<KeyRecord>>>` 抽 caller_key_id 后传入.
222pub async fn proto_request_with_idempotency_and_caller<Req, Rsp>(
223    state: &RestState,
224    proto_id: u32,
225    json_body: Option<Value>,
226    idempotency_key: Option<String>,
227    caller_key_id: Option<String>,
228) -> Result<Json<Value>, (StatusCode, Json<Value>)>
229where
230    Req: Message + Default + serde::de::DeserializeOwned,
231    Rsp: Message + Default + serde::Serialize,
232{
233    let ctx = caller_key_id.map(|k| crate::caller_context::CallerContext {
234        key_id: Some(k),
235        allowed_acc_ids: None,
236    });
237    proto_request_internal::<Req, Rsp>(
238        state,
239        proto_id,
240        json_body,
241        idempotency_key,
242        ctx.as_ref(),
243        None,
244    )
245    .await
246}
247
248/// codex 0522 F1 v1.4.106 (推荐 API): 带 `CallerContext` 的 proto_request.
249///
250/// 与 `proto_request_with_idempotency` / `proto_request_with_filter` 的关系:
251/// 后两者只接 `idempotency_key` 或 `allowed_acc_ids` 单一维度, 本函数接完整
252/// `CallerContext` (含 `key_id` + `allowed_acc_ids`), 把 caller scope **同时**
253/// 接到三个下游消费点:
254///
255/// 1. `IncomingRequest.caller_allowed_acc_ids` (dispatch handler 的 per-acc
256///    enforce, defense-in-depth)
257/// 2. `IncomingRequest.caller_key_id` (per-key 配额 / cleanup / 审计入口)
258/// 3. `FilterRegistry::apply` (响应 acc_list 过滤)
259///
260/// 任一接错 → `dispatch handler` 看到 `None` 就 silent bypass (codex F1 audit
261/// 实锤的 v1.4.105 之前 REST regression).
262///
263/// `ctx = None` 等价 `legacy mode` (无 caller key, 无 acc 限制) — 通常仅
264/// 测试 / 显式 unauthenticated route 用. 真 route handler 应该总是构 ctx
265/// 从 `Extension<Arc<KeyRecord>>` 抽出来 (`CallerContext::from_key_record`).
266pub async fn proto_request_with_ctx<Req, Rsp>(
267    state: &RestState,
268    proto_id: u32,
269    json_body: Option<Value>,
270    idempotency_key: Option<String>,
271    ctx: Option<&crate::caller_context::CallerContext>,
272) -> Result<Json<Value>, (StatusCode, Json<Value>)>
273where
274    Req: Message + Default + serde::de::DeserializeOwned,
275    Rsp: Message + Default + serde::Serialize,
276{
277    proto_request_internal::<Req, Rsp>(state, proto_id, json_body, idempotency_key, ctx, None).await
278}
279
280async fn proto_request_raw_internal<Req, Rsp>(
281    state: &RestState,
282    proto_id: u32,
283    json_body: Option<Value>,
284    idempotency_key: Option<String>,
285    ctx: Option<&crate::caller_context::CallerContext>,
286    mode: JsonRequestMode,
287) -> Result<RawJson, (StatusCode, Json<Value>)>
288where
289    Req: Message + Default + serde::de::DeserializeOwned,
290    Rsp: Message + Default + serde::Serialize,
291{
292    let req_msg: Req = decode_json_request(proto_id, json_body, mode)?;
293    let conn_id = state.next_conn_id();
294    let serial_no = state.next_serial();
295    let body = futu_server::trade_packet_id::fill_omitted_trade_packet_id_bytes(
296        proto_id,
297        req_msg.encode_to_vec(),
298        conn_id,
299        serial_no,
300    )
301    .map(Bytes::from)
302    .map_err(|e| {
303        (
304            StatusCode::INTERNAL_SERVER_ERROR,
305            Json(serde_json::json!({
306                "error": format!("failed to prepare trade PacketID: {e}")
307            })),
308        )
309    })?;
310
311    let incoming =
312        IncomingRequest::builder(conn_id, proto_id, serial_no, ProtoFmtType::Protobuf, body)
313            .with_idempotency_key(idempotency_key)
314            .with_caller_scope(
315                ctx.and_then(|c| c.caller_allowed_acc_ids_arc()),
316                ctx.and_then(|c| c.caller_key_id()),
317            )
318            .build();
319
320    let resp_bytes = state
321        .router
322        .dispatch(incoming.conn_id, &incoming)
323        .await
324        .ok_or_else(|| {
325            (
326                StatusCode::INTERNAL_SERVER_ERROR,
327                Json(serde_json::json!({
328                    "error": "handler returned no response"
329                })),
330            )
331        })?;
332
333    let resp_bytes = state.filter_registry.apply(
334        proto_id,
335        resp_bytes,
336        ctx.and_then(|c| c.allowed_acc_ids_borrow()),
337    );
338
339    let rsp_msg = Rsp::decode(Bytes::from(resp_bytes)).map_err(|e| {
340        (
341            StatusCode::INTERNAL_SERVER_ERROR,
342            Json(serde_json::json!({
343                "error": format!("failed to decode response: {e}")
344            })),
345        )
346    })?;
347
348    raw_json_from_proto_response(&rsp_msg)
349}
350
351pub(crate) fn raw_json_from_proto_response<Rsp>(
352    rsp_msg: &Rsp,
353) -> Result<RawJson, (StatusCode, Json<Value>)>
354where
355    Rsp: serde::Serialize,
356{
357    let encoded = encode_proto_response_raw_or_error_value(rsp_msg).map_err(|e| {
358        (
359            StatusCode::INTERNAL_SERVER_ERROR,
360            Json(serde_json::json!({
361                "error": e
362            })),
363        )
364    })?;
365    match encoded {
366        ProtoJsonBody::Raw(raw) => Ok(RawJson::new(raw)),
367        ProtoJsonBody::Value(value) => {
368            let raw = serde_json::to_vec(&value).map_err(|e| {
369                (
370                    StatusCode::INTERNAL_SERVER_ERROR,
371                    Json(serde_json::json!({
372                        "error": format!("failed to serialize response: {e}")
373                    })),
374                )
375            })?;
376            Ok(RawJson::new(raw))
377        }
378    }
379}
380
381/// v1.4.104 阶段 7-1 + codex 0522 F1 v1.4.106: 内部统一实现, 接 `CallerContext`
382/// 替代单 `allowed_acc_ids` 入参. 调度时同时填 `IncomingRequest.caller_key_id`
383/// 与 `caller_allowed_acc_ids`, 响应过滤共用同一个 `allowed_acc_ids` 借引用 ——
384/// 单一来源, 防 routes 层手写多套 caller scope check 漂移 (codex F2).
385async fn proto_request_internal<Req, Rsp>(
386    state: &RestState,
387    proto_id: u32,
388    json_body: Option<Value>,
389    idempotency_key: Option<String>,
390    ctx: Option<&crate::caller_context::CallerContext>,
391    surface_spec: Option<&'static EndpointSpec>,
392) -> Result<Json<Value>, (StatusCode, Json<Value>)>
393where
394    Req: Message + Default + serde::de::DeserializeOwned,
395    Rsp: Message + Default + serde::Serialize,
396{
397    // 1. JSON → protobuf 请求. Empty body still flows through EndpointSpec
398    // validation; otherwise required fields could silently become
399    // `Req::default()` and bypass REST contract checks.
400    let req_msg: Req = decode_json_request_with_surface_spec(
401        proto_id,
402        json_body,
403        JsonRequestMode::GenericRest,
404        surface_spec,
405    )?;
406
407    // 2. encode 为 protobuf bytes
408    let conn_id = state.next_conn_id();
409    let serial_no = state.next_serial();
410    let body = futu_server::trade_packet_id::fill_omitted_trade_packet_id_bytes(
411        proto_id,
412        req_msg.encode_to_vec(),
413        conn_id,
414        serial_no,
415    )
416    .map(Bytes::from)
417    .map_err(|e| {
418        (
419            StatusCode::INTERNAL_SERVER_ERROR,
420            Json(serde_json::json!({
421                "error": format!("failed to prepare trade PacketID: {e}")
422            })),
423        )
424    })?;
425
426    // 3. 构造 IncomingRequest 调用现有 handler
427    // codex 0522 F1 v1.4.106: caller_allowed_acc_ids + caller_key_id 同时从
428    // CallerContext 拿, 单一来源, 不再分别写 None.
429    let incoming =
430        IncomingRequest::builder(conn_id, proto_id, serial_no, ProtoFmtType::Protobuf, body)
431            .with_idempotency_key(idempotency_key)
432            .with_caller_scope(
433                ctx.and_then(|c| c.caller_allowed_acc_ids_arc()),
434                ctx.and_then(|c| c.caller_key_id()),
435            )
436            .build();
437
438    let resp_bytes = state
439        .router
440        .dispatch(incoming.conn_id, &incoming)
441        .await
442        .ok_or_else(|| {
443            (
444                StatusCode::INTERNAL_SERVER_ERROR,
445                Json(serde_json::json!({
446                    "error": "handler returned no response"
447                })),
448            )
449        })?;
450
451    // 3.5. v1.4.104 阶段 7-1: response filter (cross-surface 共享 FilterRegistry).
452    //      proto 2001 TRD_GET_ACC_LIST 默认注册, allowed_acc_ids 非空时 filter
453    //      响应 acc_list, 受限 key 不能跨账户 enumerate. legacy / 无限制 key /
454    //      非注册 proto_id → no-op 返原 bytes 不动.
455    // codex 0522 F2 v1.4.106: 同一 ctx.allowed_acc_ids 借引用 (与
456    // IncomingRequest.caller_allowed_acc_ids 是同一份 Arc), 单一来源.
457    let resp_bytes = state.filter_registry.apply(
458        proto_id,
459        resp_bytes,
460        ctx.and_then(|c| c.allowed_acc_ids_borrow()),
461    );
462
463    // 4. decode protobuf 响应
464    let rsp_msg = Rsp::decode(Bytes::from(resp_bytes)).map_err(|e| {
465        (
466            StatusCode::INTERNAL_SERVER_ERROR,
467            Json(serde_json::json!({
468                "error": format!("failed to decode response: {e}")
469            })),
470        )
471    })?;
472
473    // 5. 序列化为 JSON
474    let mut json_rsp = serde_json::to_value(&rsp_msg).map_err(|e| {
475        (
476            StatusCode::INTERNAL_SERVER_ERROR,
477            Json(serde_json::json!({
478                "error": format!("failed to serialize response: {e}")
479            })),
480        )
481    })?;
482
483    // 6. v1.4.34 BUG-2b 修:给所有错误响应的 ret_msg 加 `[err_code=X]` 前缀
484    //    历史:v1.4.27 server_err() helper 在 futu-trd 客户端 lib 里包了 CLI/gRPC/MCP
485    //    三个 surface,漏了 REST 这个 surface。external reviewer 4 次独立复现;v1.4.30 / 31 /
486    //    32 / 33 都没修。根因:REST 走 proto_request 直接序列化响应,没经过 server_err
487    //    helper 层。v1.4.34 直接在 JSON 层包一下,不动 daemon response(daemon 要给
488    //    CLI 客户端留原始 err_code 字段,CLI 自己会包)。
489    maybe_wrap_err_code_prefix(&mut json_rsp);
490
491    Ok(Json(json_rsp))
492}
493
494pub(super) enum ProtoJsonBody {
495    Raw(Bytes),
496    Value(Value),
497}
498
499pub(super) fn encode_proto_response_raw_or_error_value<Rsp>(
500    rsp_msg: &Rsp,
501) -> Result<ProtoJsonBody, String>
502where
503    Rsp: serde::Serialize,
504{
505    let raw =
506        serde_json::to_vec(rsp_msg).map_err(|e| format!("failed to serialize response: {e}"))?;
507    if serialized_ret_type(&raw) == Some(0) {
508        return Ok(ProtoJsonBody::Raw(Bytes::from(raw)));
509    }
510
511    let mut json_rsp =
512        serde_json::to_value(rsp_msg).map_err(|e| format!("failed to serialize response: {e}"))?;
513    maybe_wrap_err_code_prefix(&mut json_rsp);
514    Ok(ProtoJsonBody::Value(json_rsp))
515}
516
517fn serialized_ret_type(raw: &[u8]) -> Option<i64> {
518    #[derive(serde::Deserialize)]
519    struct RetOnly {
520        ret_type: Option<i64>,
521    }
522
523    serde_json::from_slice::<RetOnly>(raw)
524        .ok()
525        .and_then(|ret| ret.ret_type)
526}
527
528/// v1.4.34 BUG-2b:REST 响应的 ret_msg 包 `[err_code=X]` 前缀。
529///
530/// 与 `futu_trd::server_err()` 同语义,但作用在已序列化的 JSON 上:
531///
532/// - `ret_type == 0`:成功,不动
533/// - `ret_type != 0` 且 `err_code` 非 null:`[err_code=<code>] <原 ret_msg>`
534/// - `ret_type != 0` 且 `err_code` 缺省:`[err_code=none] <原 ret_msg>`
535/// - `ret_type != 0` 且 ret_msg 空:只留 `[err_code=<X>]` 带方括号的标签
536///
537/// 幂等(已经带 `[err_code=` 前缀的 ret_msg 不重复包)。方括号位置精确,既
538/// 便于客户端 grep,也不误伤"错误描述里恰好有方括号"的正常 msg。
539///
540/// 只对**顶层**的 `ret_type / ret_msg / err_code` 生效;嵌套在 s2c 里的状态字段
541/// 不动。
542pub(crate) fn maybe_wrap_err_code_prefix(v: &mut Value) {
543    let obj = match v.as_object_mut() {
544        Some(o) => o,
545        None => return,
546    };
547    // 只处理失败响应
548    let is_err = obj
549        .get("ret_type")
550        .and_then(|t| t.as_i64())
551        .map(|t| t != 0)
552        .unwrap_or(false);
553    if !is_err {
554        return;
555    }
556    // 读当前 msg(可能是 null)
557    let raw_msg = obj
558        .get("ret_msg")
559        .and_then(|m| m.as_str())
560        .unwrap_or("")
561        .to_string();
562    // 幂等:已带前缀就不动(多轮 middleware 不应该双包)
563    if raw_msg.starts_with("[err_code=") {
564        return;
565    }
566    // 读 err_code(可能是 null / 整数)
567    let err_code_label = match obj.get("err_code") {
568        Some(Value::Number(n)) => n
569            .as_i64()
570            .map(|i| i.to_string())
571            .unwrap_or("none".to_string()),
572        _ => "none".to_string(),
573    };
574    let new_msg = if raw_msg.is_empty() {
575        format!("[err_code={err_code_label}]")
576    } else {
577        format!("[err_code={err_code_label}] {raw_msg}")
578    };
579    obj.insert("ret_msg".to_string(), Value::String(new_msg));
580}