refactor(errors): classify auth and browser failures
This commit is contained in:
@@ -78,6 +78,22 @@ describe("ExceptionHandler", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("maps browser capability and permission errors", () => {
|
||||
expect(ExceptionHandler.handle(new Error("MediaRecorder not supported"))).toEqual({
|
||||
code: ExceptionCode.unsupported,
|
||||
message: "This feature is not supported in this browser.",
|
||||
});
|
||||
|
||||
expect(
|
||||
ExceptionHandler.handle(
|
||||
new DOMException("Microphone permission denied", "NotAllowedError"),
|
||||
),
|
||||
).toEqual({
|
||||
code: ExceptionCode.permissionDenied,
|
||||
message: "Permission denied.",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves an existing AppException result", () => {
|
||||
const error = new AppException({
|
||||
code: ExceptionCode.notFound,
|
||||
|
||||
@@ -9,6 +9,8 @@ export const ExceptionCode = {
|
||||
validation: "VALIDATION_ERROR",
|
||||
storage: "STORAGE_ERROR",
|
||||
payment: "PAYMENT_ERROR",
|
||||
permissionDenied: "PERMISSION_DENIED",
|
||||
unsupported: "UNSUPPORTED_FEATURE",
|
||||
cancelled: "CANCELLED",
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { AppException } from "./app-exception";
|
||||
import { ExceptionCode } from "./exception-code";
|
||||
import type { ExceptionResult } from "./exception-result";
|
||||
import { ApiExceptionHandler } from "./handlers/api-exception-handler";
|
||||
import { BrowserExceptionHandler } from "./handlers/browser-exception-handler";
|
||||
import {
|
||||
DEFAULT_EXCEPTION_MESSAGE,
|
||||
FallbackExceptionHandler,
|
||||
@@ -25,6 +26,10 @@ export class ExceptionHandler {
|
||||
return NetworkExceptionHandler.handle(error);
|
||||
}
|
||||
|
||||
if (BrowserExceptionHandler.canHandle(error)) {
|
||||
return BrowserExceptionHandler.handle(error);
|
||||
}
|
||||
|
||||
if (ValidationExceptionHandler.canHandle(error)) {
|
||||
return ValidationExceptionHandler.handle(error);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { ExceptionCode } from "../exception-code";
|
||||
import type { ExceptionResult } from "../exception-result";
|
||||
|
||||
export class BrowserExceptionHandler {
|
||||
static canHandle(error: unknown): error is Error | DOMException {
|
||||
if (!isErrorLike(error)) return false;
|
||||
return isPermissionError(error) || isUnsupportedFeatureError(error);
|
||||
}
|
||||
|
||||
static handle(error: Error | DOMException): ExceptionResult {
|
||||
if (isPermissionError(error)) {
|
||||
return {
|
||||
code: ExceptionCode.permissionDenied,
|
||||
message: "Permission denied.",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
code: ExceptionCode.unsupported,
|
||||
message: "This feature is not supported in this browser.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function isPermissionError(error: Error | DOMException): boolean {
|
||||
const name = error.name.toLowerCase();
|
||||
const message = error.message.toLowerCase();
|
||||
return (
|
||||
name === "notallowederror" ||
|
||||
name === "permissiondeniederror" ||
|
||||
message.includes("permission denied") ||
|
||||
(message.includes("permission") && message.includes("denied"))
|
||||
);
|
||||
}
|
||||
|
||||
function isUnsupportedFeatureError(error: Error | DOMException): boolean {
|
||||
const name = error.name.toLowerCase();
|
||||
const message = error.message.toLowerCase();
|
||||
return (
|
||||
name === "notsupportederror" ||
|
||||
message.includes("not supported") ||
|
||||
message.includes("not available on server") ||
|
||||
message.includes("unsupported")
|
||||
);
|
||||
}
|
||||
|
||||
function isErrorLike(error: unknown): error is Error | DOMException {
|
||||
if (error instanceof Error) return true;
|
||||
return typeof DOMException !== "undefined" && error instanceof DOMException;
|
||||
}
|
||||
@@ -69,7 +69,11 @@ async function refreshLoginToken(baseUrl: string): Promise<void> {
|
||||
const storage = AuthStorage.getInstance();
|
||||
const refreshResult = await storage.getRefreshToken();
|
||||
if (!Result.isOk(refreshResult) || !refreshResult.data) {
|
||||
throw new Error("No refresh token available");
|
||||
throw new ApiError(
|
||||
"HTTP_UNAUTHORIZED",
|
||||
"No refresh token available",
|
||||
ErrorCode.httpUnauthorized,
|
||||
);
|
||||
}
|
||||
|
||||
const response = await fetch(`${baseUrl}${ApiPath.refresh}`, {
|
||||
@@ -79,7 +83,12 @@ async function refreshLoginToken(baseUrl: string): Promise<void> {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Refresh failed: ${response.status}`);
|
||||
throw new ApiError(
|
||||
apiCodeForStatus(response.status),
|
||||
"Refresh failed",
|
||||
response.status,
|
||||
{ statusText: response.statusText },
|
||||
);
|
||||
}
|
||||
|
||||
const json = (await response.json()) as {
|
||||
@@ -91,7 +100,7 @@ async function refreshLoginToken(baseUrl: string): Promise<void> {
|
||||
if (json.data.token) await storage.setLoginToken(json.data.token);
|
||||
if (json.data.refreshToken) await storage.setRefreshToken(json.data.refreshToken);
|
||||
} else {
|
||||
throw new Error("Refresh response invalid");
|
||||
throw new ApiError(ErrorCode.parseError, "Refresh response invalid");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +118,12 @@ async function refreshGuestToken(baseUrl: string): Promise<void> {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Guest refresh failed: ${response.status}`);
|
||||
throw new ApiError(
|
||||
apiCodeForStatus(response.status),
|
||||
"Guest refresh failed",
|
||||
response.status,
|
||||
{ statusText: response.statusText },
|
||||
);
|
||||
}
|
||||
|
||||
const json = (await response.json()) as {
|
||||
@@ -121,7 +135,22 @@ async function refreshGuestToken(baseUrl: string): Promise<void> {
|
||||
if (json.data.token) await storage.setGuestToken(json.data.token);
|
||||
if (json.data.deviceId) await storage.setDeviceId(json.data.deviceId);
|
||||
} else {
|
||||
throw new Error("Guest refresh response invalid");
|
||||
throw new ApiError(ErrorCode.parseError, "Guest refresh response invalid");
|
||||
}
|
||||
}
|
||||
|
||||
function apiCodeForStatus(status: number): string {
|
||||
switch (status) {
|
||||
case ErrorCode.httpUnauthorized:
|
||||
return "HTTP_UNAUTHORIZED";
|
||||
case ErrorCode.httpForbidden:
|
||||
return "HTTP_FORBIDDEN";
|
||||
case ErrorCode.httpNotFound:
|
||||
return "HTTP_NOT_FOUND";
|
||||
case ErrorCode.httpServerError:
|
||||
return "HTTP_SERVER_ERROR";
|
||||
default:
|
||||
return "HTTP_ERROR";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user