gitbutlerapp/gitbutler · error · napi::Error
{e}
Error message
{e} What it means
Generated in `build_napi_params` for parameters whose type maps through a serde_json `Value` transport: `serde_json::from_value` fails while converting the JS-supplied value into the transport type, producing a napi InvalidArg error carrying serde_json's message (`{e}`). Means the JSON shape sent from JS didn't match the expected transport struct/enum.
Solutions
- Fix the JS argument so it matches the expected serde shape (field names and types).
- Regenerate/inspect the TS definitions emitted by the macro to see the expected shape.
- Log the parameter value before the call to spot mistyped fields.
- Update the Rust transport struct to accept the shape callers actually send, if the new shape is intended.
Example fix
// before
api.createAssignment({ projectId: 123, commitId: "abc" }) // numeric id not accepted
// after
api.createAssignment({ projectId: "123", commitId: "abc" }) Defensive patterns
Strategy: validation
Validate before calling
function assertMatchesShape(obj, requiredFields) {
if (obj == null || typeof obj !== 'object') throw new TypeError('expected object');
for (const f of requiredFields) {
if (obj[f] === undefined) throw new TypeError(`missing field: ${f}`);
}
return obj;
}
// call before the API:
assertMatchesShape(arg, ['projectId', 'commitId']); Type guard
function isPlainObject(v) { return v !== null && typeof v === 'object' && !Array.isArray(v); } Try / catch
try { await api.fn(arg); }
catch (e) { if (e?.code === 'InvalidArg') console.error('bad JSON shape:', arg); throw e; } Prevention
- Use the generated TypeScript definitions instead of hand-built objects.
- Keep SDK/TS types in sync with Rust transport structs after schema changes.
- Avoid camelCase/snake_case drift; copy field names from the generated .d.ts.
When it happens
Trigger: Calling a `but_api` function from JS passing an object with missing/extra/mistyped fields for a parameter that is deserialized from serde_json::Value (e.g. wrong nested types, string instead of number).
Common situations: SDK/TS type drift after a Rust struct changed; hand-built plain objects missing required fields; sending undefined/null where a field is required; camelCase vs snake_case key mismatch.
Related errors
- existing GitMeta key
- existing GitMeta key
- existing GitMeta key
- existing GitMeta key
- argument ' ' must be a non-negative integer that fits in…
AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18).
Data as JSON: /api/errors/7d986b969a83d6bd.
Report an issue: GitHub.
Appendix: source
Thrown at crates/but-api-macros/src/lib.rs:1465
let ident = &pat_ident.ident;
if let Some(context_kind) = context_param_kind(&pat_ty.ty) {
context_bindings.push(ContextParamBinding {
ident: ident.clone(),
kind: context_kind,
});
}
if let Some(custom_transport) = parse_param_transport_mapping(pat_ty)? {
let transport_ty = &custom_transport.transport_ty;
let ts_type_str = type_to_ts_name(transport_ty);
let transport_ident = format_ident!("__{}_transport", ident);
let actual_ty = &pat_ty.ty;
params.push(quote! { #[napi(ts_arg_type = #ts_type_str)] #ident: ::serde_json::Value });
names.push(ident.to_string());
conversions.push(quote! {
let #transport_ident: #transport_ty = ::serde_json::from_value(#ident)
.map_err(|e| napi::Error::new(napi::Status::InvalidArg, format!("{e}")))?;
let #ident: #actual_ty = <#actual_ty>::from(#transport_ident);
});
call_arg_idents.push(quote! { #ident });
continue;
}
if let Some(permission_kind) = permission_param_kind(&pat_ty.ty)? {
let guard_ident = permission_guard_ident(ident);
permission_bindings.push(PermissionParamBinding {
ty: (*pat_ty.ty).clone(),
kind: permission_kind,
guard_ident: guard_ident.clone(),
});
call_arg_idents.push(match permission_kind {
PermissionParamKind::Exclusive => quote! { #guard_ident.write_permission() },
PermissionParamKind::Shared => quote! { #guard_ident.read_permission() },
});
continue;View on GitHub (pinned to 58e5313667)