fix(mobile): handle RSSHub subscription limit errors
This commit is contained in:
parent
52f587dacb
commit
2350884eae
|
|
@ -0,0 +1,29 @@
|
|||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import { sanitizeErrorMessage } from "./error-message"
|
||||
|
||||
describe("sanitizeErrorMessage", () => {
|
||||
it("removes Follow API request context from display messages", () => {
|
||||
const message = [
|
||||
"RSSHub feed subscription limit exceeded",
|
||||
"Request: POST /subscriptions (original: /subscriptions)",
|
||||
"Args: {",
|
||||
' "headers": {',
|
||||
' "cookie": "session=secret"',
|
||||
" }",
|
||||
"}",
|
||||
].join("\n")
|
||||
|
||||
expect(sanitizeErrorMessage(message)).toBe("RSSHub feed subscription limit exceeded")
|
||||
})
|
||||
|
||||
it("supports CRLF request context", () => {
|
||||
expect(
|
||||
sanitizeErrorMessage("Subscription limit exceeded\r\nRequest: POST /subscriptions"),
|
||||
).toBe("Subscription limit exceeded")
|
||||
})
|
||||
|
||||
it("preserves ordinary error messages", () => {
|
||||
expect(sanitizeErrorMessage("Unable to follow this feed")).toBe("Unable to follow this feed")
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
const FOLLOW_API_REQUEST_CONTEXT_PATTERN = /\r?\nRequest:[\s\S]*$/u
|
||||
|
||||
export const sanitizeErrorMessage = (message: string) =>
|
||||
message.replace(FOLLOW_API_REQUEST_CONTEXT_PATTERN, "").trim()
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
import { FollowAPIError } from "@follow-app/client-sdk"
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
import { toastFetchError } from "./error-parser"
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
isPaymentEnabled: false,
|
||||
showUpgradeRequiredDialog: vi.fn(),
|
||||
toastError: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock("i18next", () => ({
|
||||
t: (key: string) => {
|
||||
if (key === "errors:2012") {
|
||||
return "RSSHub feed subscription limit exceeded"
|
||||
}
|
||||
return key.replace(/^errors:/u, "")
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("@/src/atoms/server-configs", () => ({
|
||||
getIsPaymentEnabled: () => mocks.isPaymentEnabled,
|
||||
}))
|
||||
|
||||
vi.mock("@/src/modules/dialogs/UpgradeRequiredDialog", () => ({
|
||||
showUpgradeRequiredDialog: mocks.showUpgradeRequiredDialog,
|
||||
}))
|
||||
|
||||
vi.mock("./toast", () => ({
|
||||
toast: {
|
||||
error: mocks.toastError,
|
||||
},
|
||||
}))
|
||||
|
||||
describe("toastFetchError", () => {
|
||||
beforeEach(() => {
|
||||
mocks.isPaymentEnabled = false
|
||||
mocks.showUpgradeRequiredDialog.mockClear()
|
||||
mocks.toastError.mockClear()
|
||||
})
|
||||
|
||||
it("shows a concise upgrade dialog for RSSHub subscription limits", () => {
|
||||
mocks.isPaymentEnabled = true
|
||||
const error = new FollowAPIError(
|
||||
[
|
||||
"RSSHub feed subscription limit exceeded",
|
||||
"Request: POST /subscriptions (original: /subscriptions)",
|
||||
'Args: { "headers": { "cookie": "session=secret" } }',
|
||||
].join("\n"),
|
||||
402,
|
||||
"2012",
|
||||
)
|
||||
|
||||
toastFetchError(error)
|
||||
|
||||
expect(mocks.showUpgradeRequiredDialog).toHaveBeenCalledWith({
|
||||
title: "RSSHub feed subscription limit exceeded",
|
||||
message: "settings:subscription.summary.free_description",
|
||||
})
|
||||
expect(mocks.toastError).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("does not expose request context when an API error code has no translation", () => {
|
||||
const error = new FollowAPIError(
|
||||
[
|
||||
"Unable to follow this feed",
|
||||
"Request: POST /subscriptions (original: /subscriptions)",
|
||||
'Args: { "headers": { "cookie": "session=secret" } }',
|
||||
].join("\n"),
|
||||
400,
|
||||
"29999",
|
||||
)
|
||||
|
||||
toastFetchError(error)
|
||||
|
||||
expect(mocks.toastError).toHaveBeenCalledOnce()
|
||||
expect(mocks.toastError).toHaveBeenCalledWith("Unable to follow this feed")
|
||||
})
|
||||
})
|
||||
|
|
@ -5,6 +5,7 @@ import { FetchError } from "ofetch"
|
|||
import { getIsPaymentEnabled } from "@/src/atoms/server-configs"
|
||||
import { showUpgradeRequiredDialog } from "@/src/modules/dialogs/UpgradeRequiredDialog"
|
||||
|
||||
import { sanitizeErrorMessage } from "./error-message"
|
||||
import { toast } from "./toast"
|
||||
|
||||
export const getFetchErrorInfo = (
|
||||
|
|
@ -21,11 +22,11 @@ export const getFetchErrorInfo = (
|
|||
const i18nKey = `errors:${code}` as any
|
||||
const i18nMessage = t(i18nKey) === i18nKey ? message : t(i18nKey)
|
||||
return {
|
||||
message: `${i18nMessage}${reason ? `: ${reason}` : ""}`,
|
||||
message: sanitizeErrorMessage(`${i18nMessage}${reason ? `: ${reason}` : ""}`),
|
||||
code,
|
||||
}
|
||||
} catch {
|
||||
return { message: error.message }
|
||||
return { message: sanitizeErrorMessage(error.message) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -35,15 +36,15 @@ export const getFetchErrorInfo = (
|
|||
const i18nKey = `errors:${code}` as any
|
||||
const i18nMessage = t(i18nKey) === i18nKey ? error.message : t(i18nKey)
|
||||
return {
|
||||
message: i18nMessage,
|
||||
message: sanitizeErrorMessage(i18nMessage),
|
||||
code,
|
||||
}
|
||||
} catch {
|
||||
return { message: error.message }
|
||||
return { message: sanitizeErrorMessage(error.message) }
|
||||
}
|
||||
}
|
||||
|
||||
return { message: error.message }
|
||||
return { message: sanitizeErrorMessage(error.message) }
|
||||
}
|
||||
|
||||
export const getFetchErrorMessage = (error: Error) => {
|
||||
|
|
@ -58,7 +59,7 @@ export const createErrorToaster = (title?: string) => (err: Error) =>
|
|||
toastFetchError(err, { title })
|
||||
|
||||
export const toastFetchError = (error: Error, { title: _title }: { title?: string } = {}) => {
|
||||
const { message: fallbackMessage } = error
|
||||
const fallbackMessage = sanitizeErrorMessage(error.message)
|
||||
let message = fallbackMessage
|
||||
let _reason = ""
|
||||
let code: number | undefined
|
||||
|
|
@ -113,6 +114,8 @@ export const toastFetchError = (error: Error, { title: _title }: { title?: strin
|
|||
}
|
||||
}
|
||||
|
||||
message = sanitizeErrorMessage(message)
|
||||
|
||||
// 2fa errors are handled by the form
|
||||
if (code === 4007 || code === 4008) {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
"2004": "Feed failed to parse",
|
||||
"2010": "Ownership challenge failed",
|
||||
"2011": "Subscription limit exceeded",
|
||||
"2012": "RSSHub feed subscription limit exceeded",
|
||||
"3000": "Entry not found",
|
||||
"4000": "Already claimed",
|
||||
"4001": "User wallet error",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
"2004": "Échec de l'analyse du flux",
|
||||
"2010": "Échec de la vérification de propriété",
|
||||
"2011": "Limite d'abonnement dépassée",
|
||||
"2012": "Limite d'abonnement aux flux RSSHub dépassée",
|
||||
"3000": "Entrée introuvable",
|
||||
"4000": "Déjà réclamé",
|
||||
"4001": "Erreur de portefeuille utilisateur",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
"2004": "フィードの解析に失敗しました",
|
||||
"2010": "所有権の確認に失敗しました",
|
||||
"2011": "購読制限を超えました",
|
||||
"2012": "RSSHubフィードの購読数制限を超えました",
|
||||
"3000": "エントリが見つかりません",
|
||||
"4000": "すでに請求されています",
|
||||
"4001": "ユーザーウォレットエラー",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
"2004": "订阅源解析失败",
|
||||
"2010": "所有权挑战失败",
|
||||
"2011": "超出订阅限制",
|
||||
"2012": "RSSHub 订阅源数量限制已超出",
|
||||
"3000": "未找到条目",
|
||||
"4000": "已被认领",
|
||||
"4001": "用户钱包错误",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
"2004": "訂閱源解析失敗",
|
||||
"2010": "所有權挑戰失敗",
|
||||
"2011": "超過訂閱限制",
|
||||
"2012": "RSSHub 訂閱源數量限制已超過",
|
||||
"3000": "條目未找到",
|
||||
"4000": "已被認領",
|
||||
"4001": "用戶錢包錯誤",
|
||||
|
|
|
|||
Loading…
Reference in New Issue