charmbracelet/crush · error

failed to decode grant permission response: %w

Error message

failed to decode grant permission response: %w

What it means

After a 200 response from /workspaces/{id}/permissions/grant, GrantPermission decodes the body into proto.PermissionGrantResponse. This error wraps a JSON decode failure: empty body, non-JSON body (HTML error page from a proxy), or a body whose shape does not match the response struct. The grant outcome is unknown because the Resolved field could not be read.

Source

Thrown at internal/client/proto.go:691

}

// GrantPermission grants a permission on a workspace. The returned
// bool reports whether this call resolved the pending request (true)
// or found it already resolved by a previous caller (false). A false
// value is not an error — it just means another subscriber resolved
// the same request first.
func (c *Client) GrantPermission(ctx context.Context, id string, req proto.PermissionGrant) (bool, error) {
	rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/permissions/grant", id), nil, jsonBody(req), http.Header{"Content-Type": []string{"application/json"}})
	if err != nil {
		return false, fmt.Errorf("failed to grant permission: %w", err)
	}
	defer rsp.Body.Close()
	if rsp.StatusCode != http.StatusOK {
		return false, fmt.Errorf("failed to grant permission: status code %d", rsp.StatusCode)
	}
	var resp proto.PermissionGrantResponse
	if err := json.NewDecoder(rsp.Body).Decode(&resp); err != nil {
		return false, fmt.Errorf("failed to decode grant permission response: %w", err)
	}
	return resp.Resolved, nil
}

// AnswerQuestionBatch submits answers for a batch question on a
// workspace. Returns true if this call resolved the pending
// request, false if already resolved by another caller.
func (c *Client) AnswerQuestionBatch(ctx context.Context, id string, req proto.QuestionAnswer) (bool, error) {
	rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/questions/answer", id), nil, jsonBody(req), http.Header{"Content-Type": []string{"application/json"}})
	if err != nil {
		return false, fmt.Errorf("failed to answer question batch: %w", err)
	}
	defer rsp.Body.Close()
	if rsp.StatusCode != http.StatusOK {
		return false, fmt.Errorf("failed to answer question batch: status code %d", rsp.StatusCode)
	}
	var resp proto.QuestionAnswerResponse
	if err := json.NewDecoder(rsp.Body).Decode(&resp); err != nil {

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Inspect the raw response body (curl the endpoint with the same payload) to see what was actually returned.
  2. Verify client and server versions match so proto.PermissionGrantResponse matches the server's response schema.
  3. Check for reverse proxies/gateways mangling the response and bypass or fix them.
  4. Retry the call; if deterministic, capture a wire dump and file a bug against the server.
Defensive patterns

Strategy: try-catch

Type guard

func isDecodeError(err error) bool {
	return err != nil && strings.Contains(err.Error(), "failed to decode grant permission response")
}

Try / catch

resolved, err := client.GrantPermission(ctx, wsID, grant)
if isDecodeError(err) {
	// 200 with unparseable body: capture for diagnosis, treat as unknown outcome
	log.Error("grant response undecodable; outcome unknown", "err", err)
	return errUnknownOutcome
}

Prevention

When it happens

Trigger: Server returns 200 with an empty/truncated body; an intermediary (reverse proxy, gateway) intercepts and returns HTML; the server serializes a different response schema than proto.PermissionGrantResponse.

Common situations: Proxy or load balancer returning an HTML error page with 200; server/client version skew changing the response JSON; gzip/encoding issues; server bug writing no body.

Understand the failure class

Related errors


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