command greet missing required key userName
主线位置:调用主线中段,
generate_handler!找到命令之后、命令函数执行之前。#[command]生成的包装为每个参数构造一个CommandItem { name, key, .. },按key从请求体里取值再反序列化(参数、返回值与 InvokeResolver)。key怎么来的在 #[command] 与 generate_handler!。取不到值,命令函数根本不会被调用,JS 侧的 Promise 被拒绝。
flowchart TB
body["请求体"] --> isobj{"是 JSON 对象吗"}
isobj -->|"原始字节"| raw["expected a value for key"]
isobj -->|是| get{"有顶层键 userName"}
get -->|没有| miss["missing required key"]
get -->|有| de["交给参数类型的 Deserialize"]
de -->|"字段名不对"| field["missing field"]
de -->|成功| run["调用命令函数"]
本页实验 crate 是 pit1-args:由模板 base 复制的隐藏窗口应用,页面逐个调用命令,把结果或拒绝值收集起来交给 report 打印。M2 Max、macOS 27、rustc 1.99.0,tauri 2.12.5,debug 构建,--features tauri/custom-protocol。
现象
Rust 侧三个命令:
#[derive(Debug, Deserialize)]
struct Query {
page_size: u32,
}
#[tauri::command]
fn greet(user_name: String) -> String {
format!("hello, {user_name}")
}
#[tauri::command]
fn search(query: Query) -> String {
format!("{query:?}")
}
#[tauri::command]
fn sum(nums: Vec<u32>) -> u32 {
nums.iter().sum()
}
页面里每个命令用两种写法各调一次,catch 里同时记下拒绝值的类型:
const cases = [
['greet', { user_name: 'rie' }],
['greet', { userName: 'rie' }],
['search', { query: { pageSize: 10 } }],
['search', { query: { page_size: 10 } }],
['sum', [1, 2, 3]],
['sum', { nums: [1, 2, 3] }],
];
for (const [cmd, args] of cases) {
const label = `${cmd}(${JSON.stringify(args)})`;
try {
lines.push(`${label} => ok ${JSON.stringify(await invoke(cmd, args))}`);
} catch (e) {
lines.push(`${label} => err (${typeof e}) ${e}`);
}
}
输出:
greet({"user_name":"rie"}) => err (string) invalid args `userName` for command `greet`: command greet missing required key userName
greet({"userName":"rie"}) => ok "hello, rie"
search({"query":{"pageSize":10}}) => err (string) invalid args `query` for command `search`: missing field `page_size`
search({"query":{"page_size":10}}) => ok "Query { page_size: 10 }"
sum([1,2,3]) => err (string) invalid args `nums` for command `sum`: command sum expected a value for key nums but the IPC call used a bytes payload
sum({"nums":[1,2,3]}) => ok 6
三种失败正好反着来:顶层参数要 camelCase,嵌套字段却要 snake_case,数组要包进对象。
出处
- 顶层键是 camelCase。宏默认
rename_all = "camelCase",把参数名user_name转成userName当key;CommandItem的deserialize_json做v.get(self.key),取不到就是command {name} missing required key {key},外面再包一层invalid args `{key}` for command `{name}`:。多余的键不检查,所以传了user_name不会有别的提示。 - 嵌套字段不转换。
key只用于取顶层的值;取出的Value交给Query自己的#[derive(Deserialize)],字段名照Query的 serde 属性,没有属性就是 Rust 里的page_size。这条错误来自 serde,原文和serde_json::from_value一样(serde 卷的 missing field)。 - 数组变成原始字节。
invoke的第二个参数是数组或 typed array 时,core.js把它当作原始字节发送,请求体不是 JSON 对象(普通数组发出去的是"1,2,3"这样的文本,见 JS 侧的 invoke)。deserialize_json遇到InvokeBody::Raw就报「expected a value for key ... but the IPC call used a bytes payload」。 - 拒绝值是字符串。
InvokeError由crate::Error::InvalidArgs的Display文本构造,JS 拿到的e是string,没有e.message;写catch (e) { console.error(e.message) }只会打出undefined。
修法
JS 侧按规则写:顶层 camelCase、嵌套字段照结构体、参数一律放进对象(上面每组的第二行)。
或者在 Rust 侧把规则改成和 JS 一致。顶层用 rename_all = "snake_case",嵌套结构体加 #[serde(rename_all = "camelCase")]:
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct QueryCamel {
page_size: u32,
}
#[tauri::command(rename_all = "snake_case")]
fn greet_snake(user_name: String) -> String {
format!("hello, {user_name}")
}
#[tauri::command]
fn search_camel(query: QueryCamel) -> u32 {
query.page_size
}
greet_snake({"user_name":"rie"}) => ok "hello, rie"
greet_snake({"userName":"rie"}) => err (string) invalid args `user_name` for command `greet_snake`: command greet_snake missing required key user_name
search_camel({"query":{"pageSize":10}}) => ok 10
rename_all 只影响顶层键;改了之后 camelCase 反而取不到。一个项目里最好只用一种约定:要么全按 tauri 的默认(顶层 camelCase,结构体都加 #[serde(rename_all = "camelCase")]),要么所有命令都写 rename_all = "snake_case"。
怎么确认没中招
- 拒绝值里的反引号部分就是答案:
invalid args `userName`说明命令要的键是userName。 - 想在 Rust 侧写测试:
tauri::test的get_ipc_response可以直接拿一段 JSON 请求体调命令,不用开窗口,参数、返回值与 InvokeResolver 里有一张逐个请求体对照的表。 - 前端统一把拒绝值当字符串处理(或者先
String(e)),别读e.message。
小结
- 参数在 JSON 里的键默认是参数名的 lowerCamelCase,取不到就是「command X missing required key Y」;多余的键不报错。
- camelCase 只管顶层;嵌套结构体的字段按它自己的 serde 属性,错误是 serde 的「missing field」。
invoke(cmd, [..])发的是原始字节,任何具名参数都取不到;参数要放进对象。- 拒绝值是字符串,没有
message。