hashicorp/nomad · error

failed to marshal command: %W

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.

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)

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.