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的unwrappanic(浏览器里 wasmunreachable,应用停止),服务端也解不出;改用结构体。 - GET 带 body 参数:原生客户端和 curl 都正常,浏览器
fetch抛TypeError;把参数写进?n。 - 参数类型不能序列化是编译期的
❌ Invalid Arguments to ServerFn ❌,报错只指到宏那一行。