fix: include Sentry API response body in provisioning errors - #38
fix: include Sentry API response body in provisioning errors#38c1-dev-bot[bot] wants to merge 1 commit into
Conversation
The AddMemberToOrganization function was discarding the Sentry API error response body, making it impossible to debug 400 Bad Request errors. The error propagated only contained "unexpected status code: 400" without the actual Sentry error (e.g., "already a member", "invalid email", "permission denied for role"). Changes: - Add readBody() helper that returns response body as a string for inclusion in error messages - Include Sentry API response body in error messages from AddMemberToOrganization and DeleteMemberFromOrganization - Detect "member already exists" responses and return a sentinel error (ErrMemberAlreadyExists) - Handle ErrMemberAlreadyExists in CreateAccount to treat already-existing members as a success instead of failing the provisioning ticket Fixes: CXH-1175
|
Claude finished @c1-dev-bot[bot]'s task in 1m 23s —— View job PR Review: Include Sentry API response body in provisioning errors
Overall: Good improvement — surfacing the API response body in errors is clearly the right call for debuggability, and handling the "already exists" case gracefully is a solid addition. A few issues to address: Issues Found
Minor / Questions
See inline comments for specific suggestions. |
| s := string(body) | ||
| l.Info("response body", zap.String("body", s)) | ||
| if len(s) > 1024 { | ||
| s = s[:1024] | ||
| } |
There was a problem hiding this comment.
nit: The full (potentially large) body is logged before truncation. Consider logging only the truncated string, or at least use Debug level instead of Info to avoid dumping large payloads into production logs.
| s := string(body) | |
| l.Info("response body", zap.String("body", s)) | |
| if len(s) > 1024 { | |
| s = s[:1024] | |
| } | |
| s := string(body) | |
| if len(s) > 1024 { | |
| s = s[:1024] | |
| } | |
| l.Debug("response body", zap.String("body", s)) |
| return fmt.Errorf("failed to add member to organization: %w", err) | ||
| } | ||
|
|
||
| defer res.Body.Close() |
There was a problem hiding this comment.
Bug: When readBody is called here, it closes res.Body via defer bodyCloser.Close(). But execution then continues to line 148 (defer res.Body.Close()), which will attempt to close the already-closed body — a double-close. While this is unlikely to cause a runtime error with most HTTP implementations, it's technically incorrect.
Since readBody already closes the body, you should return immediately after the body-related error paths, or skip the defer res.Body.Close() when the body has already been consumed. The same pattern applies to DeleteMemberFromOrganization.
One clean approach: move the defer res.Body.Close() above the status check and have readBody accept io.Reader instead of io.ReadCloser (not closing it), so the caller retains ownership of closing.
| if body != "" { | ||
| return fmt.Errorf("failed to add member to organization: %w: %s", err, body) | ||
| } |
There was a problem hiding this comment.
Bug: When the error path detects "member already exists", it returns ErrMemberAlreadyExists — a new sentinel error that does not wrap the original err. This means the original HTTP error context (status code, URL, etc.) is silently discarded. This is fine for the "already exists" case since the caller treats it as success, but worth noting.
More importantly, on line 142, the error format %w: %s produces something like "failed to add member to organization: unexpected status code 400: {"detail":"..."}". The %w wrapping is correct here, but the appended body text after the colon could be confusing since it looks like it might be part of the wrapped error chain. Consider using a clearer separator:
| if body != "" { | |
| return fmt.Errorf("failed to add member to organization: %w: %s", err, body) | |
| } | |
| if body != "" { | |
| return fmt.Errorf("failed to add member to organization: %w (response: %s)", err, body) | |
| } |
| }) | ||
| if err != nil { | ||
| if errors.Is(err, client.ErrMemberAlreadyExists) { | ||
| return &v2.CreateAccountResponse_ActionRequiredResult{}, nil, nil, nil |
There was a problem hiding this comment.
Question: CreateAccountResponse_ActionRequiredResult is also used on the success path (line 128). Is this the correct response type for the "already exists" case? Typically, per the CLAUDE.md guidance on grant idempotency, you'd want to signal "already exists" distinctly. However, since there's no CreateAccountResponse_SuccessResult with an existing resource reference being returned here either way, this seems acceptable — just want to confirm this is intentional and not a case where a SuccessResult with the existing member's resource would be more appropriate.
Summary
AddMemberToOrganizationandDeleteMemberFromOrganization, so 400/4xx errors are actually debuggable instead of just showing "unexpected status code: 400"readBody()helper that reads and returns the response body for inclusion in error messages (with 1024-byte truncation)ErrMemberAlreadyExistserrorErrMemberAlreadyExistsinCreateAccountto treat already-existing members as success rather than failing the provisioning ticketContext
An issue was reported where account provisioning failed with
400 Bad Requestbut the error message contained no details about why Sentry rejected the request. ThelogBody()function logged the response body but did not include it in the returned error, making debugging impossible without access to connector logs.Additionally, if a user is already a member of the Sentry organization, Sentry returns a 400 with "member already exists" / "already a member" in the response body. The connector now handles this gracefully by succeeding the account creation instead of failing.
Test plan
go build ./...)go test ./...)Fixes: CXH-1175
Automated PR Notice
This PR was automatically created by c1-dev-bot as a potential implementation.
This code requires: