ducaale/xh · error

Can't use file fields in JSON mode (perhaps you meant…

Error message

Can't use file fields in JSON mode (perhaps you meant --form?)

What it means

File field syntax (`key@file`) is a form/multipart concept. In default JSON mode, `body_from_file` scans the request items for any non-empty `FormFile` and rejects the request, since a JSON object body cannot absorb file uploads — the user almost certainly wanted form mode.

Solutions

  1. Add `--form`/`-f` (urlencoded) or `--multipart` to the command when using `key@file` fields.
  2. If the file content should be the entire request body, use a redirect/body syntax (`xh POST :8080 @ photo.png` with content-type) instead of a file field.
  3. Remove the `@` field if it was included by mistake.

Example fix

// before
xh POST :8080 file@photo.png
// after
xh --form POST :8080 file@photo.png
Defensive patterns

Strategy: validation

Validate before calling

# guard: key@file requires form mode
if echo "$args" | grep -qE '[A-Za-z0-9_]+@[^ ]+' && ! echo "$args" | grep -qE -- '--form|--multipart| -f( |$)'; then
  echo 'file fields need --form or --multipart'; exit 1
fi

Try / catch

match result {
    Err(e) if e.to_string().contains("file fields in JSON mode") => {
        eprintln!("add --form/-f when using key@file fields");
    }
    other => other?,
}

Prevention

When it happens

Trigger: Running `xh POST :8080 file@photo.png` without `--form`/`-f`/`--multipart` — the `FormFile` item check in `body_from_file` fires the error.

Common situations: Forgetting `-f` when uploading files; migrating curl `-F` commands to xh without adding form mode; tutorials showing `@` syntax without mentioning the required flag.

Understand the failure class

Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.

Related errors


AI-assisted analysis of ducaale/xh@2404aceecc (2026-09-13). Data as JSON: /api/errors/63276f1952731457. Report an issue: GitHub.

Appendix: source

Thrown at src/request_items.rs:407

                }
                RequestItem::HttpHeader(..) => {}
                RequestItem::HttpHeaderFromFile(..) => {}
                RequestItem::HttpHeaderToUnset(..) => {}
                RequestItem::UrlParam(..) => {}
                RequestItem::UrlParamFromFile(..) => {}
            }
        }
        Ok(Body::Multipart(form))
    }

    fn body_from_file(self) -> Result<Body> {
        let mut body = None;
        if self
            .items
            .iter()
            .any(|item| matches!(item, RequestItem::FormFile {key, ..} if !key.is_empty()))
        {
            return Err(anyhow!(
                "Can't use file fields in JSON mode (perhaps you meant --form?)"
            ));
        }
        for item in self.items {
            match item {
                RequestItem::DataField { .. }
                | RequestItem::JsonField(..)
                | RequestItem::DataFieldFromFile { .. }
                | RequestItem::JsonFieldFromFile(..) => {
                    return Err(anyhow!(
                        "Request body (from a file) and request data (key=value) cannot be mixed."
                    ));
                }
                RequestItem::FormFile {
                    key,
                    file_name,
                    file_type,
                    file_name_header,

View on GitHub (pinned to 2404aceecc)