gastownhall/beads · error

failed to marshal request body: %w

Error message

failed to marshal request body: %w

What it means

doRequest serializes the request body to JSON before sending. If the supplied body struct cannot be marshaled (unsupported types like channels, funcs, or cyclic references), the request is aborted locally and this wrapped error is returned. In practice it is nearly impossible to trigger because bodies are plain structs.

Source

Thrown at internal/gitlab/client.go:106

	if len(params) > 0 {
		values := url.Values{}
		for k, v := range params {
			values.Set(k, v)
		}
		u += "?" + values.Encode()
	}

	return u
}

// doRequest performs an HTTP request with authentication and retry logic.
func (c *Client) doRequest(ctx context.Context, method, urlStr string, body interface{}) ([]byte, http.Header, error) {
	var reqBody io.Reader
	if body != nil {
		jsonBody, err := json.Marshal(body)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to marshal request body: %w", err)
		}
		reqBody = bytes.NewReader(jsonBody)
	}

	var lastErr error
	for attempt := 0; attempt <= MaxRetries; attempt++ {
		// Reset body reader at top of loop so retries after network errors
		// don't send empty bodies (the reader may be at EOF).
		if body != nil {
			jsonBody, err := json.Marshal(body)
			if err != nil {
				return nil, nil, fmt.Errorf("failed to marshal request body: %w", err)
			}
			reqBody = bytes.NewReader(jsonBody)
		}

		req, err := http.NewRequestWithContext(ctx, method, urlStr, reqBody)
		if err != nil {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the body struct for channel/func/cyclic fields and remove or tag them `json:"-"`
  2. Ensure all body fields are JSON-serializable types (string, int, slices, maps, pointers)
  3. If passing a custom body, validate with json.Marshal in a unit test before the real call
  4. Update to the latest version in case a field type was changed upstream

Example fix

// before
type update struct{ Hook func() `json:"hook"` }
client.UpdateIssue(ctx, 42, update{Hook: fn}) // marshal fails
// after
type update struct{ Hook func() `json:"-"` }
client.UpdateIssue(ctx, 42, update{Hook: fn})
Defensive patterns

Strategy: validation

Validate before calling

if _, err := json.Marshal(body); err != nil {
	return fmt.Errorf("body not JSON-marshalable: %w", err)
}

Type guard

func jsonSafe(v interface{}) bool {
	switch v.(type) { case chan struct{}, func(): return false }
	return json.NewMarshalableCheck(v) == nil // or: json.Marshal(v) == nil error
}

Try / catch

if err != nil && strings.Contains(err.Error(), "failed to marshal request body") {
	log.Fatalf("programmer error: request body is not JSON-serializable: %v", err)
}

Prevention

When it happens

Trigger: Calling any Client method (CreateIssue, UpdateIssue, etc.) with a body containing fields that encoding/json cannot marshal: channels, funcs, cycles, or invalid types in custom struct fields passed via extension points.

Common situations: Forking/monkey-patching the client with a body struct containing unexported-but-problematic types; embedding a sync.Mutex by value is fine, but embedding a channel or a func-typed field without json tags pointing at it; hand-building request bodies in tests.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/ea2e241b06cedffe. Report an issue: GitHub.