Skip to content

CryptoOnramp SDK: API Error Refactor - #13156

Merged
Twigz merged 73 commits into
masterfrom
twigz/crypto-onramp-errors
Jun 8, 2026
Merged

CryptoOnramp SDK: API Error Refactor#13156
Twigz merged 73 commits into
masterfrom
twigz/crypto-onramp-errors

Conversation

@Twigz

@Twigz Twigz commented May 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

Refactors API errors to adopt what's being proposed in Proposal: Errors are a UI for attestation errors.

We added a new abstract error type CryptoOnrampException with two concrete cases so far:

  • AppAttestationException: This uses app attestation failures as a first step to adopt the new API surface for errors according to the linked doc. While there's no attestation-specific error properties on AppAttestationException, they could be added to this concrete associated value, keying off type to know to parse them.
  • UncategorizedApiErrorException: Captures all other API errors that we're not specifically targeting yet so that they share a common API surface to other API errors.

CryptoOnrampException exposes userMessage and developerMessage for all types.

Motivation

Proposal: Errors are a UI

Testing

  • Added tests
  • Modified tests
  • Manually verified

Changelog

Updated

@Twigz
Twigz requested a review from jeanregisser May 26, 2026 21:13

@jeanregisser jeanregisser left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is looking very close to the direction I had in mind, and it lines up well with the iOS shape now: concrete rich errors, API-specific context, safe message / userMessage, richer developerMessage, and a shared renderer for the final diagnostic format.

One thing I’d like to get your take on: should the common rich-error contract be source-agnostic and named similarly to iOS, e.g. StripeCryptoOnrampError?

Right now CryptoOnrampException is the base rich-error type, but because it extends StripeException, the common error surface is effectively tied to API-backed failures. That works well for AppAttestationException / UncategorizedApiErrorException, but I think we’ll want future non-API rich errors to participate in the same rendering / DevX model without needing to pretend they are Stripe API exceptions.

Maybe the split could be:

interface StripeCryptoOnrampError {
    val code: String
    val userMessage: String
    val developerMessage: String
    val sdkVersion: String
    val docUrl: String?
    val underlyingError: Throwable?
}

Then API-backed errors can still extend StripeException and implement this interface.

The main downside I see is catch ergonomics: callers can’t catch (e: StripeCryptoOnrampError) if it’s only an interface, so they’d need to catch Throwable / Exception and check if (e is StripeCryptoOnrampError). That may or may not be worth it, but I think it keeps the rich-error contract cleaner and better aligned with the longer-term direction.

Curious what you think. I don’t feel strongly that this PR needs to solve every future case, but I’d like us to avoid making the shared rich-error shape API-specific if we can.

@Twigz

Twigz commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator Author

This is looking very close to the direction I had in mind, and it lines up well with the iOS shape now: concrete rich errors, API-specific context, safe message / userMessage, richer developerMessage, and a shared renderer for the final diagnostic format.

One thing I’d like to get your take on: should the common rich-error contract be source-agnostic and named similarly to iOS, e.g. StripeCryptoOnrampError?

Right now CryptoOnrampException is the base rich-error type, but because it extends StripeException, the common error surface is effectively tied to API-backed failures. That works well for AppAttestationException / UncategorizedApiErrorException, but I think we’ll want future non-API rich errors to participate in the same rendering / DevX model without needing to pretend they are Stripe API exceptions.

Maybe the split could be:

interface StripeCryptoOnrampError {
    val code: String
    val userMessage: String
    val developerMessage: String
    val sdkVersion: String
    val docUrl: String?
    val underlyingError: Throwable?
}

Then API-backed errors can still extend StripeException and implement this interface.

The main downside I see is catch ergonomics: callers can’t catch (e: StripeCryptoOnrampError) if it’s only an interface, so they’d need to catch Throwable / Exception and check if (e is StripeCryptoOnrampError). That may or may not be worth it, but I think it keeps the rich-error contract cleaner and better aligned with the longer-term direction.

Curious what you think. I don’t feel strongly that this PR needs to solve every future case, but I’d like us to avoid making the shared rich-error shape API-specific if we can.

I went back and forth on this, especially around the conformance to StripeException in general. I've switched to doing what you suggested here, as it's probably the most correct.

@Twigz
Twigz requested a review from jeanregisser June 2, 2026 16:33
@Twigz
Twigz marked this pull request as ready for review June 2, 2026 16:33
@Twigz
Twigz requested review from a team as code owners June 2, 2026 16:33
jeanregisser
jeanregisser previously approved these changes Jun 7, 2026

@jeanregisser jeanregisser left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is looking very aligned with the iOS direction now. I like the split between the source-agnostic StripeCryptoOnrampError interface and API-backed CryptoOnrampApiException; that feels like the right foundation for future non-API rich errors too.

A couple small alignment points I’d consider:

  1. Can StripeCryptoOnrampError.code be non-null? I think every rich error should have a stable SDK-owned code. For API errors, we can still prefer the backend code when present, but concrete errors should provide a fallback like iOS does.

  2. Can we keep userMessage SDK-owned for now, and preserve backend user_message only as API context? Right now Android uses apiUserMessage ?: fallbackUserMessage, while iOS always uses SDK-owned copy for AppAttestationAPIError / UncategorizedAPIError and exposes apiUserMessage separately. We may later decide API v2 user_message should take precedence, but I’d rather make that decision intentionally across both SDKs.

Overall this feels like a strong foundation and very close to where we want this to land.

@Twigz
Twigz merged commit 94db387 into master Jun 8, 2026
51 checks passed
@Twigz
Twigz deleted the twigz/crypto-onramp-errors branch June 8, 2026 18:32
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.

2 participants