{"record":{"id":"745223fb95fe70e7","repo":"charmbracelet/crush","slug":"failed-to-grant-permission-w","errorCode":null,"errorMessage":"failed to grant permission: %w","messagePattern":"failed to grant permission: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/client/proto.go","lineNumber":683,"sourceCode":"\tif rsp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"failed to get sessions: status code %d\", rsp.StatusCode)\n\t}\n\tvar sessions []proto.Session\n\tif err := json.NewDecoder(rsp.Body).Decode(&sessions); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to decode sessions: %w\", err)\n\t}\n\treturn sessions, nil\n}\n\n// GrantPermission grants a permission on a workspace. The returned\n// bool reports whether this call resolved the pending request (true)\n// or found it already resolved by a previous caller (false). A false\n// value is not an error — it just means another subscriber resolved\n// the same request first.\nfunc (c *Client) GrantPermission(ctx context.Context, id string, req proto.PermissionGrant) (bool, error) {\n\trsp, err := c.post(ctx, fmt.Sprintf(\"/workspaces/%s/permissions/grant\", id), nil, jsonBody(req), http.Header{\"Content-Type\": []string{\"application/json\"}})\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"failed to grant permission: %w\", err)\n\t}\n\tdefer rsp.Body.Close()\n\tif rsp.StatusCode != http.StatusOK {\n\t\treturn false, fmt.Errorf(\"failed to grant permission: status code %d\", rsp.StatusCode)\n\t}\n\tvar resp proto.PermissionGrantResponse\n\tif err := json.NewDecoder(rsp.Body).Decode(&resp); err != nil {\n\t\treturn false, fmt.Errorf(\"failed to decode grant permission response: %w\", err)\n\t}\n\treturn resp.Resolved, nil\n}\n\n// AnswerQuestionBatch submits answers for a batch question on a\n// workspace. Returns true if this call resolved the pending\n// request, false if already resolved by another caller.\nfunc (c *Client) AnswerQuestionBatch(ctx context.Context, id string, req proto.QuestionAnswer) (bool, error) {\n\trsp, err := c.post(ctx, fmt.Sprintf(\"/workspaces/%s/questions/answer\", id), nil, jsonBody(req), http.Header{\"Content-Type\": []string{\"application/json\"}})\n\tif err != nil {","sourceCodeStart":665,"sourceCodeEnd":701,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/client/proto.go#L665-L701","documentation":"GrantPermission POSTs a permission grant to /workspaces/{id}/permissions/grant. This error wraps any transport-level failure returned by c.post (DNS failure, connection refused, TLS error, request-encoding failure, context cancellation) before an HTTP response is available. It signals the grant request never completed at the HTTP layer, so the permission was not resolved by this client.","triggerScenarios":"Calling GrantPermission when the Crush server is unreachable, the workspace base URL/host is wrong, the network drops mid-request, or ctx is cancelled before the POST completes.","commonSituations":"Server process not running or restarted; misconfigured server address in client setup; firewall/proxy blocking the port; laptop offline or VPN down; request deadline exceeded while the caller waited too long.","solutions":["Verify the Crush server is running and reachable at the configured address (curl its health/base endpoint).","Check and correct the client's base URL/port configuration used to build the Client.","Retry GrantPermission; the grant may have been resolved by a previous caller (returns false, nil) so handle a false result as success.","If timeouts recur, increase the context deadline or investigate network/proxy issues."],"exampleFix":"// before\nresolved, err := client.GrantPermission(ctx, wsID, grant)\nif err != nil { log.Fatal(err) }\n// after\nresolved, err := client.GrantPermission(ctx, wsID, grant)\nif err != nil {\n\tif errors.Is(err, context.DeadlineExceeded) {\n\t\t// server unreachable or too slow; retry or surface connectivity error\n\t}\n\treturn fmt.Errorf(\"granting permission %s: %w\", grant.ID, err)\n}\nif !resolved { /* already resolved by another subscriber; not an error */ }","handlingStrategy":"retry","validationCode":"// Check server reachability before granting\nfunc serverReachable(ctx context.Context, baseURL string) error {\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL, nil)\n\tif err != nil { return err }\n\trsp, err := http.DefaultClient.Do(req)\n\tif err != nil { return err }\n\tdefer rsp.Body.Close()\n\treturn nil\n}","typeGuard":null,"tryCatchPattern":"resolved, err := client.GrantPermission(ctx, wsID, grant)\nif err != nil {\n\tif errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {\n\t\treturn err // propagate cancellation\n\t}\n\t// transient network failure: safe to retry (Resolved prevents double-grant)\n\treturn retryGrant(ctx, wsID, grant, 3)\n}\nif !resolved { log.Info(\"already resolved by another subscriber\") }","preventionTips":["Health-check the server before issuing grants","Use a context with a sane timeout (e.g. 10s) instead of one that expires mid-request","Treat a false Resolved return as success — never retry-loop on it","Keep client base URL/port in one config location to avoid drift"],"tags":["http-client","network","grpc-like-rpc","permissions"],"backgroundTag":"http-request-failed","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}