Skip to content

Commit 2350884

Browse files
committed
fix(mobile): handle RSSHub subscription limit errors
1 parent 52f587d commit 2350884

9 files changed

Lines changed: 126 additions & 6 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { describe, expect, it } from "vitest"
2+
3+
import { sanitizeErrorMessage } from "./error-message"
4+
5+
describe("sanitizeErrorMessage", () => {
6+
it("removes Follow API request context from display messages", () => {
7+
const message = [
8+
"RSSHub feed subscription limit exceeded",
9+
"Request: POST /subscriptions (original: /subscriptions)",
10+
"Args: {",
11+
' "headers": {',
12+
' "cookie": "session=secret"',
13+
" }",
14+
"}",
15+
].join("\n")
16+
17+
expect(sanitizeErrorMessage(message)).toBe("RSSHub feed subscription limit exceeded")
18+
})
19+
20+
it("supports CRLF request context", () => {
21+
expect(
22+
sanitizeErrorMessage("Subscription limit exceeded\r\nRequest: POST /subscriptions"),
23+
).toBe("Subscription limit exceeded")
24+
})
25+
26+
it("preserves ordinary error messages", () => {
27+
expect(sanitizeErrorMessage("Unable to follow this feed")).toBe("Unable to follow this feed")
28+
})
29+
})
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
const FOLLOW_API_REQUEST_CONTEXT_PATTERN = /\r?\nRequest:[\s\S]*$/u
2+
3+
export const sanitizeErrorMessage = (message: string) =>
4+
message.replace(FOLLOW_API_REQUEST_CONTEXT_PATTERN, "").trim()
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { FollowAPIError } from "@follow-app/client-sdk"
2+
import { beforeEach, describe, expect, it, vi } from "vitest"
3+
4+
import { toastFetchError } from "./error-parser"
5+
6+
const mocks = vi.hoisted(() => ({
7+
isPaymentEnabled: false,
8+
showUpgradeRequiredDialog: vi.fn(),
9+
toastError: vi.fn(),
10+
}))
11+
12+
vi.mock("i18next", () => ({
13+
t: (key: string) => {
14+
if (key === "errors:2012") {
15+
return "RSSHub feed subscription limit exceeded"
16+
}
17+
return key.replace(/^errors:/u, "")
18+
},
19+
}))
20+
21+
vi.mock("@/src/atoms/server-configs", () => ({
22+
getIsPaymentEnabled: () => mocks.isPaymentEnabled,
23+
}))
24+
25+
vi.mock("@/src/modules/dialogs/UpgradeRequiredDialog", () => ({
26+
showUpgradeRequiredDialog: mocks.showUpgradeRequiredDialog,
27+
}))
28+
29+
vi.mock("./toast", () => ({
30+
toast: {
31+
error: mocks.toastError,
32+
},
33+
}))
34+
35+
describe("toastFetchError", () => {
36+
beforeEach(() => {
37+
mocks.isPaymentEnabled = false
38+
mocks.showUpgradeRequiredDialog.mockClear()
39+
mocks.toastError.mockClear()
40+
})
41+
42+
it("shows a concise upgrade dialog for RSSHub subscription limits", () => {
43+
mocks.isPaymentEnabled = true
44+
const error = new FollowAPIError(
45+
[
46+
"RSSHub feed subscription limit exceeded",
47+
"Request: POST /subscriptions (original: /subscriptions)",
48+
'Args: { "headers": { "cookie": "session=secret" } }',
49+
].join("\n"),
50+
402,
51+
"2012",
52+
)
53+
54+
toastFetchError(error)
55+
56+
expect(mocks.showUpgradeRequiredDialog).toHaveBeenCalledWith({
57+
title: "RSSHub feed subscription limit exceeded",
58+
message: "settings:subscription.summary.free_description",
59+
})
60+
expect(mocks.toastError).not.toHaveBeenCalled()
61+
})
62+
63+
it("does not expose request context when an API error code has no translation", () => {
64+
const error = new FollowAPIError(
65+
[
66+
"Unable to follow this feed",
67+
"Request: POST /subscriptions (original: /subscriptions)",
68+
'Args: { "headers": { "cookie": "session=secret" } }',
69+
].join("\n"),
70+
400,
71+
"29999",
72+
)
73+
74+
toastFetchError(error)
75+
76+
expect(mocks.toastError).toHaveBeenCalledOnce()
77+
expect(mocks.toastError).toHaveBeenCalledWith("Unable to follow this feed")
78+
})
79+
})

apps/mobile/src/lib/error-parser.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { FetchError } from "ofetch"
55
import { getIsPaymentEnabled } from "@/src/atoms/server-configs"
66
import { showUpgradeRequiredDialog } from "@/src/modules/dialogs/UpgradeRequiredDialog"
77

8+
import { sanitizeErrorMessage } from "./error-message"
89
import { toast } from "./toast"
910

1011
export const getFetchErrorInfo = (
@@ -21,11 +22,11 @@ export const getFetchErrorInfo = (
2122
const i18nKey = `errors:${code}` as any
2223
const i18nMessage = t(i18nKey) === i18nKey ? message : t(i18nKey)
2324
return {
24-
message: `${i18nMessage}${reason ? `: ${reason}` : ""}`,
25+
message: sanitizeErrorMessage(`${i18nMessage}${reason ? `: ${reason}` : ""}`),
2526
code,
2627
}
2728
} catch {
28-
return { message: error.message }
29+
return { message: sanitizeErrorMessage(error.message) }
2930
}
3031
}
3132

@@ -35,15 +36,15 @@ export const getFetchErrorInfo = (
3536
const i18nKey = `errors:${code}` as any
3637
const i18nMessage = t(i18nKey) === i18nKey ? error.message : t(i18nKey)
3738
return {
38-
message: i18nMessage,
39+
message: sanitizeErrorMessage(i18nMessage),
3940
code,
4041
}
4142
} catch {
42-
return { message: error.message }
43+
return { message: sanitizeErrorMessage(error.message) }
4344
}
4445
}
4546

46-
return { message: error.message }
47+
return { message: sanitizeErrorMessage(error.message) }
4748
}
4849

4950
export const getFetchErrorMessage = (error: Error) => {
@@ -58,7 +59,7 @@ export const createErrorToaster = (title?: string) => (err: Error) =>
5859
toastFetchError(err, { title })
5960

6061
export const toastFetchError = (error: Error, { title: _title }: { title?: string } = {}) => {
61-
const { message: fallbackMessage } = error
62+
const fallbackMessage = sanitizeErrorMessage(error.message)
6263
let message = fallbackMessage
6364
let _reason = ""
6465
let code: number | undefined
@@ -113,6 +114,8 @@ export const toastFetchError = (error: Error, { title: _title }: { title?: strin
113114
}
114115
}
115116

117+
message = sanitizeErrorMessage(message)
118+
116119
// 2fa errors are handled by the form
117120
if (code === 4007 || code === 4008) {
118121
return

locales/errors/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"2004": "Feed failed to parse",
2020
"2010": "Ownership challenge failed",
2121
"2011": "Subscription limit exceeded",
22+
"2012": "RSSHub feed subscription limit exceeded",
2223
"3000": "Entry not found",
2324
"4000": "Already claimed",
2425
"4001": "User wallet error",

locales/errors/fr-FR.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"2004": "Échec de l'analyse du flux",
2020
"2010": "Échec de la vérification de propriété",
2121
"2011": "Limite d'abonnement dépassée",
22+
"2012": "Limite d'abonnement aux flux RSSHub dépassée",
2223
"3000": "Entrée introuvable",
2324
"4000": "Déjà réclamé",
2425
"4001": "Erreur de portefeuille utilisateur",

locales/errors/ja.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"2004": "フィードの解析に失敗しました",
2020
"2010": "所有権の確認に失敗しました",
2121
"2011": "購読制限を超えました",
22+
"2012": "RSSHubフィードの購読数制限を超えました",
2223
"3000": "エントリが見つかりません",
2324
"4000": "すでに請求されています",
2425
"4001": "ユーザーウォレットエラー",

locales/errors/zh-CN.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"2004": "订阅源解析失败",
2020
"2010": "所有权挑战失败",
2121
"2011": "超出订阅限制",
22+
"2012": "RSSHub 订阅源数量限制已超出",
2223
"3000": "未找到条目",
2324
"4000": "已被认领",
2425
"4001": "用户钱包错误",

locales/errors/zh-TW.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"2004": "訂閱源解析失敗",
2020
"2010": "所有權挑戰失敗",
2121
"2011": "超過訂閱限制",
22+
"2012": "RSSHub 訂閱源數量限制已超過",
2223
"3000": "條目未找到",
2324
"4000": "已被認領",
2425
"4001": "用戶錢包錯誤",

0 commit comments

Comments
 (0)