charmbracelet/crush · error

failed to decode messages: %w

Error message

failed to decode messages: %w

What it means

Thrown by Client.ListMessages when the 200 response body from GET /workspaces/{id}/sessions/{sessionID}/messages cannot be JSON-decoded into []proto.Message. Unlike GetSessionInfo, an empty body (io.EOF) is tolerated, so this error means the body was non-empty but malformed or structurally incompatible with proto.Message. Called via restoreModelFromSession.

Source

Thrown at internal/client/proto.go:602

	if rsp.StatusCode != http.StatusOK {
		return fmt.Errorf("failed to initiate session agent processing: status code %d", rsp.StatusCode)
	}
	return nil
}

// ListMessages retrieves all messages for a session as proto types.
func (c *Client) ListMessages(ctx context.Context, id string, sessionID string) ([]proto.Message, error) {
	rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s/messages", id, sessionID), nil, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to get messages: %w", err)
	}
	defer rsp.Body.Close()
	if rsp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("failed to get messages: status code %d", rsp.StatusCode)
	}
	var msgs []proto.Message
	if err := json.NewDecoder(rsp.Body).Decode(&msgs); err != nil && !errors.Is(err, io.EOF) {
		return nil, fmt.Errorf("failed to decode messages: %w", err)
	}
	return msgs, nil
}

// GetSession retrieves a specific session as a proto type.
func (c *Client) GetSession(ctx context.Context, id string, sessionID string) (*proto.Session, error) {
	rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s", id, sessionID), nil, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to get session: %w", err)
	}
	defer rsp.Body.Close()
	if rsp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("failed to get session: status code %d", rsp.StatusCode)
	}
	var sess proto.Session
	if err := json.NewDecoder(rsp.Body).Decode(&sess); err != nil {
		return nil, fmt.Errorf("failed to decode session: %w", err)
	}

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Use errors.As to extract json.UnmarshalTypeError and identify which field of proto.Message is incompatible.
  2. Align client and server versions so the Message JSON schema matches.
  3. Bypass any proxy/middlebox and curl the endpoint directly to see the raw body.
  4. If old rows are the cause, migrate or discard the incompatible session data on the server.

Example fix

// before
msgs, err := client.ListMessages(ctx, wsID, sessionID)
if err != nil {
    return err
}
// after: narrow down the incompatible field
msgs, err := client.ListMessages(ctx, wsID, sessionID)
if err != nil {
    var ute *json.UnmarshalTypeError
    if errors.As(err, &ute) {
        return fmt.Errorf("message field %q has wrong type (version skew?): %w", ute.Field, err)
    }
    return err
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Probe the endpoint and verify the body parses as a message array
rsp, err := http.Get(serverURL + "/workspaces/" + wsID + "/sessions/" + sessionID + "/messages")
if err == nil && rsp.StatusCode == 200 {
    var probe []json.RawMessage
    if json.NewDecoder(rsp.Body).Decode(&probe) != nil {
        return errors.New("server returned malformed messages payload — check version skew")
    }
    rsp.Body.Close()
}

Type guard

func isMessageSchemaError(err error) (string, bool) {
    var ute *json.UnmarshalTypeError
    if errors.As(err, &ute) {
        return ute.Field, true
    }
    return "", false
}

Try / catch

msgs, err := client.ListMessages(ctx, wsID, sessionID)
if err != nil {
    if field, ok := isMessageSchemaError(err); ok {
        return fmt.Errorf("incompatible message schema at field %q — upgrade client/server to matching versions", field)
    }
    return err
}

Prevention

When it happens

Trigger: Server returns 200 with a corrupted/truncated messages payload; a proxy rewrites the body (e.g. HTML error injected); server schema change where a field type no longer matches proto.Message (json.UnmarshalTypeError); binary garbage instead of JSON.

Common situations: Client/server version skew after an upgrade changed the message schema; middlebox (antivirus/proxy) mangling responses; DB rows written by an older version that no longer unmarshal into the current proto.Message type; disk corruption on the server.

Understand the failure class

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/4847a4d10e7b4153. Report an issue: GitHub.