Rust in Everything
基于 dioxus 0.7.10

tried to serialize a value before serializing key

主线位置:调用主线的客户端编码一步。宏按路由字符串把参数分成三份:路径 {id} 拼进 URL,?a&b 用 serde_qs 进查询串,其余全部进 JSON body,和 HTTP 方法无关(server function 的「参数怎么编码」)。下面三种写法都能通过宏,只有一种在编译期报错。

flowchart TB
  arg["函数参数"] --> where{"路由里写在哪"}
  where -->|"{id}"| path["拼进路径"]
  where -->|"?a 或 ?:p"| qs["serde_qs 进查询串"]
  where -->|没写| body["JSON body"]
  qs --> prim{"?:p 是基本类型"}
  prim -->|是| p1["客户端 unwrap panic"]
  body --> getq{"方法是 GET"}
  getq -->|是| p2["浏览器 TypeError"]

实验应用 pit2-fs(dioxus = "=0.7.10",dx 0.7.9)。浏览器结果来自 release 构建:页面挂载后在 use_effect 里调一次函数,把结果写进 <p>,headless Chrome 经 DevTools 协议读控制台、网络和 innerText(cdp.mjs)。原生客户端 pit2-client 用 #[path] 引入同一份 api.rs,不开 server feature。M2 Max、macOS 27、rustc 1.99.0、Chrome 156。

现象

catch-all 查询配基本类型

#[get("/api/q?:n")]
pub async fn catch_all(n: i32) -> Result<i32, ServerFnError> {
  Ok(n)
}

?:n 的意思是「整个查询串就是 n」。原生客户端调 catch_all(7):

catch_all(7) = ...

thread 'main' (2196969) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dioxus-fullstack-0.7.10/src/client.rs:30:69:
called `Result::unwrap()` on an `Err` value: Custom("tried to serialize a value before serializing key")
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

请求还没发出去。浏览器里(release)是 wasm 的 unreachable,没有网络请求,<p> 一直是空的,整个应用停在那里:

===== /args/prim
+  315ms [net] GET /args/prim
+  317ms [net<-] 200 /args/prim
+  349ms [exception] RuntimeError: unreachable
--- innerText of body ---

服务端那一半也不通,curl 两种写法都是 500:

### curl /api/q?7
HTTP/1.1 500 Internal Server Error
content-type: application/json

{"error":"error deserializing server function results: cannot deserialize primitive at the top level.Try deserializing into a struct."}

### curl /api/q?n=7
HTTP/1.1 500 Internal Server Error
content-type: application/json

{"error":"error deserializing server function results: cannot deserialize primitive at the top level.Try deserializing into a struct."}

GET 留着 body 参数

#[get("/api/getbody")]
pub async fn get_body(n: i32) -> Result<i32, ServerFnError> {
  Ok(n + 1)
}

路由字符串里没有写 n,它就进 JSON body。原生客户端(reqwest 允许 GET 带 body)得到 get_body(1) = Ok(2),curl -X GET -d '{"n":1}' 也是 200 2。浏览器里:

===== /args/body
+  313ms [net] GET /args/body
+  315ms [net<-] 200 /args/body
--- innerText of body ---
get_body(1) = Err(Request(Request("TypeError: Failed to construct 'Request': Request with GET/HEAD method cannot have body.")))

本地测试(原生客户端、curl、服务端渲染时的直接调用)全都正常,只有在浏览器里点了才失败。

参数类型不能序列化

// a type without Serialize / Deserialize
#[derive(Debug, Clone)]
pub struct Filter {
  pub tag: String,
}

#[post("/api/search")]
async fn search(filter: Filter) -> Result<Vec<String>, ServerFnError> {
  Ok(vec![filter.tag])
}

这一种在编译期就报出来(客户端那次编译,cargo build --bin badarg):

error[E0277]: ❌ Invalid Arguments to ServerFn ❌
              
              The arguments to the server function must be either:
              
              - a single `impl FromRequest + IntoRequest` argument
              - or multiple `DeserializeOwned` arguments.
              
              Did you forget to implement `IntoRequest` or `Deserialize` for one of the arguments?
              
              `IntoRequest` is a trait that allows payloads to be sent to the server function.
              
              > See https://dioxuslabs.com/learn/0.7/essentials/fullstack/server_functions for more details.
              
              
   --> src/bin/badarg.rs:9:1
    |
  9 | #[post("/api/search")]
    | ^^^^^^^^^^^^^^^^^^^^^^ the trait `AssertCanEncode` is not implemented for `CantEncode`
    |
help: the trait `AssertCanEncode` is implemented for `EncodeIsVerified`
   --> ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dioxus-fullstack-0.7.10/src/request.rs:194:1
    |
194 | impl AssertCanEncode for EncodeIsVerified {}
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: required by a bound in `dioxus::dioxus_fullstack::assert_can_encode`
   --> ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dioxus-fullstack-0.7.10/src/request.rs:216:35
    |
216 | pub fn assert_can_encode(_t: impl AssertCanEncode) {}
    |                                   ^^^^^^^^^^^^^^^
    = note: this error originates in the attribute macro `post` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0277`.
error: could not compile `exp-dioxus-pit2-client` (bin "badarg") due to 1 previous error

报错指向宏那一行,不指向哪个参数。修法就是给参数类型加 #[derive(Serialize, Deserialize)]。

出处

  • catch-all:ClientRequest::new 对 serde_qs::to_string(params) 直接 unwrap。普通查询参数被包进一个结构体,catch-all 不包,n 自己就是顶层值;serde_qs 的顶层只能是结构体或 map,基本类型没有键可写,返回 Error::Custom("tried to serialize a value before serializing key")。服务端用 serde_qs::from_str 解顶层基本类型同样失败。
  • GET body:分参数的 CompiledRoute::from_route 不看方法。wasm 上请求由 new_gloo_request 交给浏览器的 fetch,Request 构造函数对 GET / HEAD 带 body 直接抛 TypeError,dioxus 把它包成 ServerFnError::Request 返回,函数调用得到 Err。
  • 编译错误:最低一层的「兜底」编码实现返回 CantEncode,宏生成的检查调用 assert_can_encode,它要求的 trait 带着 #[diagnostic::on_unimplemented(message = "❌ Invalid Arguments to ServerFn ❌" ...)](autoderef 特化的机制见 server function 的「自动特化」)。

前两种宏都不拦:宏只看语法,类型能不能当顶层查询串、方法允不允许 body,是运行期才知道的事。

修法

catch-all 用结构体:

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Page {
  pub n: i32,
}

#[get("/api/q2?:p")]
pub async fn catch_all_struct(p: Page) -> Result<i32, ServerFnError> {
  Ok(p.n)
}

GET 的参数写进路由字符串的查询部分:

#[get("/api/getq?n")]
pub async fn get_query(n: i32) -> Result<i32, ServerFnError> {
  Ok(n + 1)
}

浏览器里重跑:

===== /args/query
+  316ms [net] GET /args/query
+  318ms [net<-] 200 /args/query
+  349ms [net] GET /api/getq?n=1&
+  351ms [net<-] 200 /api/getq?n=1&
--- innerText of body ---
get_query(1) = Ok(2)
===== /args/struct
+  313ms [net] GET /args/struct
+  314ms [net<-] 200 /args/struct
+  343ms [net] GET /api/q2?n=7&
+  345ms [net<-] 200 /api/q2?n=7&
--- innerText of body ---
catch_all_struct(Page { n: 7 }) = Ok(7)

查询串末尾多一个 &,是 dioxus 拼 URL 的方式,服务端照常解析。参数多、或者有嵌套结构时,换成 #[post] 让它们走 JSON body,比挤进查询串省事。

怎么确认没中招

  • 搜 #[get(:路由字符串里没有列出的参数都会进 body,GET 不应该有这样的参数。
  • 搜 ?: 和 ?{:catch-all 的参数类型必须是结构体或 map。
  • 至少在浏览器里调用一次每个 GET server function。原生客户端和 curl 测不出 GET body 的问题,SSR 时函数被直接调用,也测不出前两种问题。

小结

  • 参数去哪只看路由字符串:路径段、?a&b、?:p,其余进 JSON body,GET 也一样。
  • catch-all ?:p 配基本类型:客户端在 client.rs:30 的 unwrap panic(浏览器里 wasm unreachable,应用停止),服务端也解不出;改用结构体。
  • GET 带 body 参数:原生客户端和 curl 都正常,浏览器 fetch 抛 TypeError;把参数写进 ?n。
  • 参数类型不能序列化是编译期的 ❌ Invalid Arguments to ServerFn ❌,报错只指到宏那一行。

💬 关联讨论

发起讨论
加载中...