hashicorp/nomad · error

failed to marshal command

Error message

failed to marshal command: %W

What it means

In api/allocations_exec.go:94, startConnection json.Marshal's the exec command struct before embedding it as a query parameter. Marshal of a plain struct with strings/bools essentially never fails, so this error is practically unreachable; if it fires, the ExecStreamingInput/command structure contains a value json cannot encode. Note the format verb is a (invalid) capitalized %W — it still behaves like %v, so the wrapped error is not unwrappable via errors.Is/As.

Solutions

  1. Inspect the wrapped error to identify which field fails to marshal
  2. Ensure all fields of the exec command struct are JSON-encodable (strings, bools, slices)
  3. If you patched the package, revert the unsupported field or add a MarshalJSON method
  4. Report upstream if this occurs with an unmodified client — it indicates a version incompatibility

Example fix

// before (in a patched struct)
Command struct{ Args []string; Notify chan int `json:"notify"` }
// after: remove or encode the unmarshalable field
Command struct{ Args []string }
Defensive patterns

Strategy: validation

Validate before calling

if _, err := json.Marshal(cmd); err != nil {
	return fmt.Errorf("command not encodable: %w", err)
}

Type guard

func jsonEncodable(v any) bool {
	b, err := json.Marshal(v)
	return err == nil && b != nil
}

Try / catch

conn, err := s.startConnection()
if err != nil && strings.Contains(err.Error(), "failed to marshal command") {
	return fmt.Errorf("client bug: exec command struct not JSON-encodable: %w", err)
}

Prevention

When it happens

Trigger: Calling Exec/ActionExec where s.command fails to JSON-marshal — only possible if the command struct is modified to include unsupported types (channels, funcs, cyclic data). Not triggerable with the standard library's command types.

Common situations: Forking/patching the Nomad API package and adding a field with an unsupported type to the exec command struct; building the command with a custom type lacking MarshalJSON.

Understand the failure class

Background: json.Marshal / "failed to marshal" errors in Go: why "unsupported type" happens and how to fix it — this error's family across 22 libraries.

Related errors


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

Appendix: source

Thrown at api/allocations_exec.go:94

func (s *execSession) startConnection() (*websocket.Conn, error) {
	// First, attempt to connect to the node directly, but may fail due to network isolation
	// and network errors.  Fallback to using server-side forwarding instead.
	nodeClient, err := s.client.GetNodeClientWithTimeout(s.alloc.NodeID, ClientConnTimeout, s.q)
	if err == NodeDownErr {
		return nil, NodeDownErr
	}

	q := s.q
	if q == nil {
		q = &QueryOptions{}
	}
	if q.Params == nil {
		q.Params = make(map[string]string)
	}

	commandBytes, err := json.Marshal(s.command)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal command: %W", err)
	}

	q.Params["tty"] = strconv.FormatBool(s.tty)
	q.Params["task"] = s.task
	q.Params["command"] = string(commandBytes)
	reqPath := fmt.Sprintf("/v1/client/allocation/%s/exec", s.alloc.ID)

	if s.action != "" {
		q.Params["action"] = s.action
		q.Params["allocID"] = s.alloc.ID
		q.Params["group"] = s.alloc.TaskGroup
		reqPath = fmt.Sprintf("/v1/job/%s/action", url.PathEscape(s.job))
	}

	var conn *websocket.Conn

	if nodeClient != nil {
		conn, _, _ = nodeClient.websocket(reqPath, q) //nolint:bodyclose // gorilla/websocket Dialer.DialContext() does not require the body to be closed.

View on GitHub (pinned to 482b49bf1a)