hashicorp/nomad · error

failed to marshal command into json: %v

Error message

failed to marshal command into json: %v

What it means

This error comes from allocExec in command/agent/alloc_endpoint.go:597 when the `command` query parameter of the exec HTTP endpoint fails json.Unmarshal into a []string. The comment in the code notes this 'shouldn't happen' for well-formed JSON, so it fires when the caller passes a command parameter that is not a JSON array of strings (e.g. a bare word, malformed JSON, or an array with non-string elements).

Source

Thrown at command/agent/alloc_endpoint.go:597

	if rpcErr != nil {
		if structs.IsErrNoNodeConn(rpcErr) || structs.IsErrUnknownAllocation(rpcErr) || structs.IsErrUnknownNode(rpcErr) {
			rpcErr = CodedError(404, rpcErr.Error())
		}
	}

	return reply.Results, rpcErr
}

func (s *HTTPServer) allocExec(allocID string, resp http.ResponseWriter, req *http.Request) (any, error) {
	// Build the request and parse the ACL token
	task := req.URL.Query().Get("task")
	cmdJsonStr := req.URL.Query().Get("command")
	var command []string
	err := json.Unmarshal([]byte(cmdJsonStr), &command)
	if err != nil {
		// this shouldn't happen, []string is always be serializable to json
		return nil, fmt.Errorf("failed to marshal command into json: %v", err)
	}

	ttyB := false
	if tty := req.URL.Query().Get("tty"); tty != "" {
		ttyB, err = strconv.ParseBool(tty)
		if err != nil {
			return nil, fmt.Errorf("tty value is not a boolean: %v", err)
		}
	}

	args := cstructs.AllocExecRequest{
		AllocID: allocID,
		Task:    task,
		Cmd:     command,
		Tty:     ttyB,
	}
	s.parse(resp, req, &args.QueryOptions.Region, &args.QueryOptions)

View on GitHub (pinned to 482b49bf1a)

Solutions

  1. JSON-encode the command as an array of strings in the query parameter: command=%5B%22ls%22%2C%22-la%22%5D (i.e. ["ls","-la"]).
  2. Use the official Nomad CLI (`nomad alloc exec`) or API client, which encodes the parameter correctly, instead of hand-building URLs.
  3. Validate the JSON with a quick marshal/unmarshal locally (e.g. `echo '["ls"]' | jq .`) before sending the request.
  4. Check that the shell/curl quoting preserves the double quotes in the JSON array.

Example fix

// before: unencoded command
curl 'http://localhost:4646/v1/client/allocation/abc/exec?command=ls+-la&task=web'
// after: JSON-encoded string array
curl 'http://localhost:4646/v1/client/allocation/abc/exec?command=%5B%22ls%22%2C%22-la%22%5D&task=web'
Defensive patterns

Strategy: validation

Validate before calling

cmdJSON, err := json.Marshal([]string{"ls", "-la"})
if err != nil { return err }
u := fmt.Sprintf(".../exec?allocID=%s&task=%s&command=%s",
    url.QueryEscape(allocID), url.QueryEscape(task), url.QueryEscape(string(cmdJSON)))

Type guard

func isStringArray(v []interface{}) ([]string, bool) {
    out := make([]string, 0, len(v))
    for _, e := range v {
        s, ok := e.(string)
        if !ok { return nil, false }
        out = append(out, s)
    }
    return out, true
}

Try / catch

var command []string
if err := json.Unmarshal([]byte(cmdJSON), &command); err != nil {
    return fmt.Errorf("command must be a JSON array of strings: %w", err)
}

Prevention

When it happens

Trigger: Calling GET /v1/client/allocation/<allocID>/exec with a `command` query parameter that is not valid JSON or not a JSON array of strings — e.g. command=ls (no JSON quoting), command=["ls"-x], command=[1,2], or a URL-encoded value whose quotes were stripped by an intermediary.

Common situations: Hand-constructing the exec URL without JSON-encoding the command; shell/quoting issues where double quotes around the JSON array are lost; older custom tooling sending a space-separated command string instead of a JSON array.

Related errors


AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04). Data as JSON: /api/errors/49d0088b9c41b7ad. Report an issue: GitHub.