34c00e945d
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
126 lines
4.4 KiB
TypeScript
126 lines
4.4 KiB
TypeScript
import {LexError} from '@atproto/lex'
|
|
import {t} from '@lingui/core/macro'
|
|
|
|
import {isNetworkError} from '#/lib/network-error'
|
|
import {isXrpcError} from '#/lib/xrpc-error'
|
|
|
|
export {isNetworkError} from '#/lib/network-error'
|
|
|
|
/**
|
|
* The text to show the user when no special case applies.
|
|
*
|
|
* A `LexError` stringifies as `Class: [ErrorCode] message` (for example
|
|
* `XrpcResponseError: [HandleNotAvailable] Handle already taken`), so its
|
|
* `toString()` is never fit for display. Its `message` is the server's
|
|
* human-readable text, so prefer that and fall back to the lexicon code for the
|
|
* errors that carry no message.
|
|
*
|
|
* Everything else keeps the historical behaviour of stringifying and dropping a
|
|
* leading `Error: `.
|
|
*/
|
|
function toDisplayString(e: unknown, str: string): string {
|
|
if (e instanceof LexError) {
|
|
return e.message || e.error
|
|
}
|
|
if (str.startsWith('Error: ')) {
|
|
return str.slice('Error: '.length)
|
|
}
|
|
return str
|
|
}
|
|
|
|
export function cleanError(e: unknown): string {
|
|
if (!e) {
|
|
return ''
|
|
}
|
|
/*
|
|
* Match against the full stringification rather than the display text: it
|
|
* contains the error name and the lexicon code as well as the message, so a
|
|
* single check covers both error shapes.
|
|
*/
|
|
// oxlint-disable-next-line typescript/no-base-to-string
|
|
const str = typeof e === 'string' ? e : e.toString()
|
|
// the original value, not `str`, so wrapped causes are checked
|
|
if (isNetworkError(e)) {
|
|
return t`Unable to connect. Please check your internet connection and try again.`
|
|
}
|
|
/*
|
|
* The legacy client named these with spaces ("Upstream Failure"); lexicon error
|
|
* codes are space-free ("UpstreamFailure"). Match both while the app throws
|
|
* both shapes.
|
|
*/
|
|
if (
|
|
str.includes('Upstream Failure') ||
|
|
str.includes('UpstreamFailure') ||
|
|
str.includes('NotEnoughResources') ||
|
|
str.includes('pipethrough network error')
|
|
) {
|
|
return t`The server appears to be experiencing issues. Please try again in a few moments.`
|
|
}
|
|
/**
|
|
* @see https://github.com/bluesky-social/atproto/blob/255cfcebb54332a7129af768a93004e22c6858e3/packages/pds/src/actor-store/preference/transactor.ts#L24
|
|
*/
|
|
if (
|
|
str.includes('Do not have authorization to set preferences') &&
|
|
str.includes('app.bsky.actor.defs#personalDetailsPref')
|
|
) {
|
|
return t`You cannot update your birthdate while using an app password. Please sign in with your main password to update your birthdate.`
|
|
}
|
|
if (str.includes('Bad token scope') || str.includes('Bad token method')) {
|
|
return t`This feature is not available while using an App Password. Please sign in with your main password.`
|
|
}
|
|
if (str.includes('Account has been suspended')) {
|
|
return t`Account has been suspended`
|
|
}
|
|
if (str.includes('Account is deactivated')) {
|
|
return t`Account is deactivated`
|
|
}
|
|
if (str.includes('Profile not found')) {
|
|
return t`Profile not found`
|
|
}
|
|
if (str.includes('Unable to resolve handle')) {
|
|
return t`Unable to resolve handle`
|
|
}
|
|
return toDisplayString(e, str)
|
|
}
|
|
|
|
/**
|
|
* The PDS answers an app-password-scope rejection with the lexicon code
|
|
* `InvalidToken` and a message of 'Bad token scope' or 'Bad token method'
|
|
* (pipethrough), so the typed path matches the code AND the message. The
|
|
* pre-migration check compared against 'TokenInvalid', which the PDS never
|
|
* sends - the string fallback was doing all the work.
|
|
*/
|
|
export function isErrorMaybeAppPasswordPermissions(e: unknown) {
|
|
if (isXrpcError(e)) {
|
|
return (
|
|
e.error === 'InvalidToken' &&
|
|
(e.message.includes('Bad token scope') ||
|
|
e.message.includes('Bad token method'))
|
|
)
|
|
}
|
|
const str = String(e)
|
|
return str.includes('Bad token scope') || str.includes('Bad token method')
|
|
}
|
|
|
|
/**
|
|
* Intended to capture "User cancelled" or "Crop cancelled" errors
|
|
* that we often get from expo modules such @bsky.app/expo-image-crop-tool
|
|
*
|
|
* The exact name has changed in the past so let's just see if the string
|
|
* contains "cancel"
|
|
*/
|
|
export function isCancelledError(e: unknown) {
|
|
const str = String(e).toLowerCase()
|
|
return str.includes('cancel')
|
|
}
|
|
|
|
// TODO Replace this with error.shouldRetry() when available. -dsb
|
|
const RETRYABLE_ERRORS = [408, 425, 429, 500, 502, 503, 504, 522, 524]
|
|
export function isRetryableHttpStatus(status: number) {
|
|
return RETRYABLE_ERRORS.includes(status)
|
|
}
|
|
|
|
export function shouldRetryError(e: unknown) {
|
|
return isXrpcError(e) && e.shouldRetry()
|
|
}
|