Rust in Everything
基于 tauri 2.12.5

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。

💬 关联讨论

发起讨论
加载中...