Skip to content

Return Unauthenticated gRPC code for password login failures - #111

Draft
johnallers wants to merge 3 commits into
mainfrom
jallers/fix-auth-error-retryability
Draft

Return Unauthenticated gRPC code for password login failures#111
johnallers wants to merge 3 commits into
mainfrom
jallers/fix-auth-error-retryability

Conversation

@johnallers

@johnallers johnallers commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Summary

  • The password login path in Initialize() returns bare SalesforceError with no gRPC status code, causing the sync workflow to treat auth failures (e.g. deactivated user) as transient and retry indefinitely
  • Now returns codes.Unauthenticated so the sync workflow stops retrying on credential errors
  • The OAuth token path is left unchanged — TokenSource.Token() already returns properly coded gRPC errors from the control plane (e.g. FailedPrecondition), and overriding would mask transient errors that should be retried

Test plan

  • go build ./... passes
  • go test ./... passes (28 tests across 6 packages)
  • Verify connectors with deactivated service accounts stop retrying after deploy

🤖 Generated with Claude Code

… retries

Both the OAuth token and password login paths in Initialize() were returning
bare errors with no gRPC status code. When these errors bubbled up through
fmt.Errorf wrapping, the control plane could not distinguish auth failures
from transient errors and would retry indefinitely.

Now both paths return codes.Unauthenticated so the sync workflow knows to
stop retrying on credential/token errors.
@johnallers
johnallers requested a review from a team April 16, 2026 14:14
@github-actions

github-actions Bot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Claude finished @johnallers's task in 1m 57s —— View job


PR Review: Return Unauthenticated gRPC code on auth failures

  • Gather context and read changed files
  • Analyze the diff against origin/main
  • Review code quality, correctness, and patterns
  • Post review feedback

Verdict: Looks good -- small, targeted fix that solves a real problem. One consideration worth noting below.

What the change does

Both authentication paths in Initialize() (TokenSource.Token() and LoginPassword) now return status.Errorf(codes.Unauthenticated, ...) instead of bare errors. This lets the SDK's retry logic (retry.go:59) distinguish auth failures from transient errors -- it only retries codes.Unavailable and codes.DeadlineExceeded, so Unauthenticated correctly stops the retry loop.

gRPC status survives wrapping -- verified

I checked the Ping() call chain (salesforce.go:157-159):

err := c.Initialize(ctx)
if err != nil {
    return nil, fmt.Errorf("salesforce-connector: failed to initialize client: %w", err)
}

The fmt.Errorf with %w wraps the status error, but the vendored gRPC status.FromError() (status.go:112-124) uses errors.As to unwrap and find the embedded gRPC status. So status.Code(wrappedErr) correctly returns codes.Unauthenticated. The other callers in request.go pass the error through directly without wrapping, so those are fine too.

Consideration: transient errors during auth

Both TokenSource.Token() and LoginPassword() can fail for reasons beyond bad credentials -- e.g., a network timeout reaching the OAuth provider, or a temporary Salesforce API outage. Returning codes.Unauthenticated for all errors from these calls means transient failures during auth won't be retried either.

In practice this is probably the right trade-off: the vast majority of these errors are genuine auth failures (expired tokens, revoked credentials), and the previous behavior of infinite retries on credential errors was significantly worse. But it's worth knowing this exists -- if you see connectors failing on transient network blips during initialization, this is where to look.

A future refinement could inspect the error type (e.g., net.Error with Timeout()) and only return Unauthenticated for non-transient failures, but that's not necessary for this PR.

.gitignore change

Adding .monitor-connector.db is fine -- standard local dev artifact.

Summary

Clean, minimal fix. The gRPC status propagation is correct through the wrapping chain. Approve.

@github-actions

github-actions Bot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Claude finished @johnallers's task in 1m 14s —— View job


PR Review: Return Unauthenticated gRPC code on auth failures

  • Gather context and read changed files
  • Analyze the diff against origin/main
  • Review code quality, correctness, and patterns
  • Post review feedback

Verdict: Approve -- clean, minimal fix that correctly addresses the infinite retry problem.

What the change does

Both auth paths in Initialize() (TokenSource.Token() at line 128 and LoginPassword at line 141) now return status.Errorf(codes.Unauthenticated, ...) instead of bare errors. This lets the SDK's retry logic correctly identify these as non-retryable failures.

gRPC status propagation -- verified

I traced the full call chain:

  1. Retry logic (vendor/.../retry/retry.go:59): ShouldWaitAndRetry only retries codes.Unavailable and codes.DeadlineExceeded. Any other code (including Unauthenticated) returns false immediately -- no retry.

  2. fmt.Errorf wrapping in Ping() (salesforce.go:159): The %w verb wraps the status error, but status.FromError() (status.go:113) uses errors.As to unwrap the chain and recover the embedded gRPC status. So status.Code(wrappedErr) correctly returns codes.Unauthenticated.

  3. Other callers (request.go:45,117,158,179): These pass the Initialize() error through directly without wrapping, so the gRPC status is preserved as-is.

Code quality

  • The status and codes imports are correctly added and properly used
  • Error messages are descriptive ("failed to get oauth token", "could not login") and include the original error via %s on err.Error()
  • Using %s with err.Error() instead of %w is correct here -- status.Errorf doesn't support %w wrapping, and the gRPC status itself becomes the wrapping mechanism

Consideration: transient errors during auth

Both TokenSource.Token() and LoginPassword() can fail for non-auth reasons (network timeouts, temporary Salesforce API outages). Returning codes.Unauthenticated for all failures from these calls means transient errors during auth won't be retried.

This is likely the right trade-off -- the vast majority of these failures are genuine auth errors, and the previous behavior of infinite retries on bad credentials was strictly worse. If transient auth failures become a problem, a future refinement could inspect the error type (e.g., net.Error with Timeout()) before choosing the gRPC code.

Summary

Small, correct fix. The gRPC status propagates correctly through all call paths, and the retry logic will now correctly stop on auth failures.

@johnallers
johnallers requested a review from a team April 16, 2026 14:33
@johnallers
johnallers marked this pull request as draft April 16, 2026 14:51
The OAuth token source already returns properly coded gRPC errors from
the control plane (e.g. FailedPrecondition). Overriding to
Unauthenticated would mask the original code and prevent transient
errors from being retried. Only the password login path needs
Unauthenticated since LoginPassword returns bare errors with no gRPC
code.
@johnallers johnallers changed the title Return Unauthenticated gRPC code on auth failures to prevent infinite retries Return Unauthenticated gRPC code for password login failures Apr 16, 2026
@github-actions

github-actions Bot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Claude finished @johnallers's task in 1m 14s —— View job


PR Review: Return Unauthenticated gRPC code on auth failures

  • Gather context and read changed files
  • Analyze the diff against origin/main
  • Review code quality, correctness, and patterns
  • Post review feedback

Verdict: Approve -- clean, well-reasoned fix. The commit history shows good iteration.

What changed

The LoginPassword path in Initialize() (salesforce.go:141) now returns status.Errorf(codes.Unauthenticated, ...) instead of a bare error. This lets the SDK retry logic at retry.go:59 correctly identify it as non-retryable (only codes.Unavailable and codes.DeadlineExceeded trigger retries).

Commit history shows good judgment

The evolution across three commits is worth noting:

  1. 0ccd5af — Initially wrapped both auth paths (TokenSource.Token() and LoginPassword) with codes.Unauthenticated
  2. 5e8433d — Reverted the OAuth token path, recognizing that TokenSource.Token() already returns properly coded gRPC errors from the control plane (e.g., FailedPrecondition). Overriding those would mask the original code and prevent legitimate transient retries.

This is the right call. Only LoginPassword returns bare errors with no gRPC code — that's the only path that needs the fix.

gRPC status propagation — verified

I traced all callers of Initialize():

  • Ping() (salesforce.go:157-159): Wraps with fmt.Errorf("...: %w", err). The %w verb preserves the error chain, and status.FromError() (status.go:113) uses errors.As to unwrap and recover the gRPC status. codes.Unauthenticated survives.
  • request.go (lines 45, 117, 158, 179): All pass the error through directly without wrapping — gRPC status preserved as-is.

Code quality

  • Imports are correct and minimal (codes + status)
  • Error message is descriptive and includes the original error via %s on err.Error() — correct since status.Errorf doesn't support %w
  • The existing logger.Error call is preserved, so the detailed error is still logged before returning the status-coded version

No issues found

This is a small, correct fix. No bugs, no security concerns, no regressions.

@mateovespConductor
mateovespConductor requested a review from a team April 17, 2026 20:38

@mateoHernandez123 mateoHernandez123 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! 🚀

@mateoHernandez123
mateoHernandez123 dismissed their stale review July 17, 2026 19:43

Dismissing — reviewed a stale PR by mistake.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants