Use typed backend errors for translations

This commit is contained in:
n200534
2026-08-20 01:11:30 +05:30
parent eff8bb16a0
commit 3efec5ce58
38 changed files with 164 additions and 79 deletions

View File

@@ -1,6 +1,7 @@
import { i18n } from "@lingui/core"
import type { AxiosError } from "axios"
import { describe, expect, it } from "vitest"
import { loginErrorToStrings } from "./client"
import { afterEach, describe, expect, it, vi } from "vitest"
import { errorToStrings } from "./client"
const axiosError = (status: number, data: unknown) =>
({
@@ -8,16 +9,28 @@ const axiosError = (status: number, data: unknown) =>
response: { status, data },
}) as AxiosError
describe("loginErrorToStrings", () => {
it("uses the translated message for authentication errors", () => {
const error = axiosError(401, { message: "wrong username or password" })
describe("errorToStrings", () => {
afterEach(() => vi.restoreAllMocks())
expect(loginErrorToStrings(error, "Translated authentication error")).toEqual(["Translated authentication error"])
it("translates known application error types", () => {
vi.spyOn(i18n, "_").mockReturnValue("Translated authentication error")
const error = axiosError(401, {
type: "WRONG_USERNAME_OR_PASSWORD",
message: "wrong username or password",
})
expect(errorToStrings(error)).toEqual(["Translated authentication error"])
})
it("preserves backend messages for unexpected errors", () => {
const error = axiosError(500, { message: "unexpected error" })
expect(loginErrorToStrings(error, "Translated authentication error")).toEqual(["unexpected error"])
expect(errorToStrings(error)).toEqual(["unexpected error"])
})
it("preserves backend messages for unknown application error types", () => {
const error = axiosError(400, { type: "UNKNOWN_ERROR", message: "unknown error" })
expect(errorToStrings(error)).toEqual(["unknown error"])
})
})

View File

@@ -1,3 +1,5 @@
import { i18n, type MessageDescriptor } from "@lingui/core"
import { msg } from "@lingui/core/macro"
import axios, { type AxiosError } from "axios"
import type {
AddCategoryRequest,
@@ -6,6 +8,8 @@ import type {
Category,
CategoryModificationRequest,
CollapseRequest,
CommaFeedApplicationError,
CommaFeedExceptionType,
Entries,
FeedInfo,
FeedInfoRequest,
@@ -31,6 +35,10 @@ import type {
UserModel,
} from "./types"
const applicationErrorMessages = {
WRONG_USERNAME_OR_PASSWORD: msg`Wrong username or password`,
} satisfies Record<CommaFeedExceptionType, MessageDescriptor>
const axiosInstance = axios.create({ baseURL: "./rest", withCredentials: true })
axiosInstance.interceptors.response.use(
response => response,
@@ -128,21 +136,23 @@ export const errorToStrings = (err: unknown) => {
let strings: string[] = []
if (axios.isAxiosError(err) && err.response) {
if (typeof err.response.data === "string") strings.push(err.response.data)
if (isMessageError(err)) strings.push(err.response.data.message)
if (isMessageArrayError(err)) strings = [...strings, ...err.response.data.errors]
if (isCommaFeedApplicationError(err)) {
strings.push(i18n._(applicationErrorMessages[err.response.data.type]))
} else {
if (typeof err.response.data === "string") strings.push(err.response.data)
if (isMessageError(err)) strings.push(err.response.data.message)
if (isMessageArrayError(err)) strings = [...strings, ...err.response.data.errors]
}
}
return strings
}
/**
* Transform a login error into messages that can be displayed to the user.
* Authentication failures use a client-provided message so it can be translated.
*/
export const loginErrorToStrings = (err: unknown, authenticationErrorMessage: string) => {
if (isAuthenticationError(err)) return [authenticationErrorMessage]
return errorToStrings(err)
function isCommaFeedApplicationError(err: AxiosError): err is AxiosError<CommaFeedApplicationError> {
const data = err.response?.data
if (!data || typeof data !== "object" || !("type" in data)) return false
const type = data.type
return typeof type === "string" && Object.hasOwn(applicationErrorMessages, type)
}
function isMessageError(err: AxiosError): err is AxiosError<{ message: string }> {

View File

@@ -338,3 +338,10 @@ export interface AuthenticationError {
message: string
allowRegistrations: boolean
}
export type CommaFeedExceptionType = "WRONG_USERNAME_OR_PASSWORD"
export interface CommaFeedApplicationError {
type: CommaFeedExceptionType
message: string
}