mirror of
https://github.com/fluxerapp/fluxer.git
synced 2026-09-02 21:04:06 +03:00
fix(platform): parse api error responses in one place (#2355)
This commit is contained in:
@@ -8,7 +8,7 @@ import GatewayConnection from '@app/features/gateway/transport/GatewayConnection
|
||||
import {http} from '@app/features/platform/transport/RestTransport';
|
||||
import {HttpError} from '@app/features/platform/types/EndpointError';
|
||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import {failureCode} from '@app/features/platform/utils/ResponseInspection';
|
||||
import {failureCode, ipAuthorizationRequiredResponseFromError} from '@app/features/platform/utils/ResponseInspection';
|
||||
import UserSettings from '@app/features/user/state/UserSettings';
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import type {ValueOf} from '@fluxer/constants/src/ValueOf';
|
||||
@@ -210,17 +210,28 @@ function tokenBody(token: string): {token: string} {
|
||||
return {token};
|
||||
}
|
||||
|
||||
export class MalformedIpAuthorizationChallengeError extends HttpError {
|
||||
constructor(error: HttpError) {
|
||||
super({
|
||||
method: error.method,
|
||||
path: error.path,
|
||||
status: error.status,
|
||||
body: error.body,
|
||||
responseHeaders: error.responseHeaders,
|
||||
});
|
||||
this.name = 'MalformedIpAuthorizationChallengeError';
|
||||
}
|
||||
}
|
||||
|
||||
function loginIpAuthorizationResponse(error: HttpError): IpAuthorizationRequiredResponse | null {
|
||||
if (error.status !== 403 || failureCode(error) !== APIErrorCodes.IP_AUTHORIZATION_REQUIRED) {
|
||||
return null;
|
||||
}
|
||||
const body = error.body as Record<string, unknown> | undefined;
|
||||
return {
|
||||
ip_authorization_required: true,
|
||||
ticket: body?.ticket as string,
|
||||
email: body?.email as string,
|
||||
resend_available_in: (body?.resend_available_in as number) ?? 30,
|
||||
};
|
||||
const challenge = ipAuthorizationRequiredResponseFromError(error);
|
||||
if (challenge === null) {
|
||||
throw new MalformedIpAuthorizationChallengeError(error);
|
||||
}
|
||||
return challenge;
|
||||
}
|
||||
|
||||
function verificationResultFromError(
|
||||
|
||||
@@ -1,45 +1,126 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {HttpError} from '@app/features/platform/types/EndpointError';
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {HttpStatus} from '@fluxer/constants/src/HttpConstants';
|
||||
|
||||
const DEFAULT_IP_AUTHORIZATION_RESEND_SECONDS = 30;
|
||||
|
||||
const NON_CONTEXT_KEYS = new Set(['code', 'details', 'errors', 'message', 'request_id', 'retry_after', 'status']);
|
||||
|
||||
interface ValidationFault {
|
||||
path: string;
|
||||
code?: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ApiErrorResponse {
|
||||
status: number | undefined;
|
||||
code: string | undefined;
|
||||
message: string | undefined;
|
||||
requestId: string | undefined;
|
||||
fields: ReadonlyArray<ValidationFault> | undefined;
|
||||
retryAfterSeconds: number | undefined;
|
||||
context: Record<string, unknown> | undefined;
|
||||
}
|
||||
|
||||
export interface IpAuthorizationRequiredResponse {
|
||||
ip_authorization_required: true;
|
||||
ticket: string;
|
||||
email: string;
|
||||
resend_available_in: number;
|
||||
}
|
||||
|
||||
export function parseAPIErrorResponse(body: unknown, httpStatus?: number): ApiErrorResponse | null {
|
||||
if (!isRecord(body)) return null;
|
||||
const details = isRecord(body.details) ? body.details : undefined;
|
||||
const retry = details != null && isRecord(details.retry) ? details.retry : undefined;
|
||||
return {
|
||||
status: httpStatus ?? readNumber(body, 'status'),
|
||||
code: readString(body, 'code'),
|
||||
message: readString(body, 'message'),
|
||||
requestId: readString(body, 'request_id'),
|
||||
fields: readValidationFaults(details?.fields) ?? readValidationFaults(body.errors),
|
||||
retryAfterSeconds: readNumber(retry, 'after_seconds') ?? readNumber(body, 'retry_after'),
|
||||
context: readErrorContext(body, details),
|
||||
};
|
||||
}
|
||||
|
||||
export function replyCode(body: unknown): string | undefined {
|
||||
return readString(body, 'code');
|
||||
return parseAPIErrorResponse(body)?.code;
|
||||
}
|
||||
|
||||
export function replyMessage(body: unknown): string | undefined {
|
||||
return readString(body, 'message');
|
||||
return parseAPIErrorResponse(body)?.message;
|
||||
}
|
||||
|
||||
export function replyRetryAfter(body: unknown): number | undefined {
|
||||
return readNumber(body, 'retry_after');
|
||||
return parseAPIErrorResponse(body)?.retryAfterSeconds;
|
||||
}
|
||||
|
||||
export function failureCode(error: unknown): string | undefined {
|
||||
return replyCode(failureBody(error));
|
||||
return failureResponse(error)?.code;
|
||||
}
|
||||
|
||||
export function failureMessage(error: unknown): string | undefined {
|
||||
return replyMessage(failureBody(error));
|
||||
return failureResponse(error)?.message;
|
||||
}
|
||||
|
||||
export function failureRetryAfter(error: unknown): number | undefined {
|
||||
return replyRetryAfter(failureBody(error));
|
||||
return failureResponse(error)?.retryAfterSeconds;
|
||||
}
|
||||
|
||||
export function failureValidationErrors(error: unknown): ReadonlyArray<ValidationFault> | undefined {
|
||||
const body = failureBody(error);
|
||||
if (!isRecord(body)) return undefined;
|
||||
const errors = body.errors;
|
||||
return Array.isArray(errors) ? (errors as ReadonlyArray<ValidationFault>) : undefined;
|
||||
return failureResponse(error)?.fields;
|
||||
}
|
||||
|
||||
function failureBody(error: unknown): unknown {
|
||||
return error instanceof HttpError ? error.body : undefined;
|
||||
export function ipAuthorizationRequiredResponseFromError(error: unknown): IpAuthorizationRequiredResponse | null {
|
||||
const response = failureResponse(error);
|
||||
if (response == null || response.status !== HttpStatus.FORBIDDEN) return null;
|
||||
if (response.code !== APIErrorCodes.IP_AUTHORIZATION_REQUIRED) return null;
|
||||
const context = response.context;
|
||||
if (context == null) return null;
|
||||
const ticket = readString(context, 'ticket');
|
||||
const email = readString(context, 'email');
|
||||
if (ticket == null || ticket.length === 0 || email == null || email.length === 0) return null;
|
||||
const resendAvailableIn = readNumber(context, 'resend_available_in');
|
||||
if (resendAvailableIn != null && (!Number.isSafeInteger(resendAvailableIn) || resendAvailableIn < 0)) return null;
|
||||
return {
|
||||
ip_authorization_required: true,
|
||||
ticket,
|
||||
email,
|
||||
resend_available_in: resendAvailableIn ?? DEFAULT_IP_AUTHORIZATION_RESEND_SECONDS,
|
||||
};
|
||||
}
|
||||
|
||||
function failureResponse(error: unknown): ApiErrorResponse | null {
|
||||
return error instanceof HttpError ? parseAPIErrorResponse(error.body, error.status) : null;
|
||||
}
|
||||
|
||||
function readErrorContext(
|
||||
body: Record<string, unknown>,
|
||||
details: Record<string, unknown> | undefined,
|
||||
): Record<string, unknown> | undefined {
|
||||
if (details != null && isRecord(details.context)) return details.context;
|
||||
const context: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(body)) {
|
||||
if (NON_CONTEXT_KEYS.has(key)) continue;
|
||||
context[key] = value;
|
||||
}
|
||||
return Object.keys(context).length > 0 ? context : undefined;
|
||||
}
|
||||
|
||||
function readValidationFaults(value: unknown): ReadonlyArray<ValidationFault> | undefined {
|
||||
if (!Array.isArray(value)) return undefined;
|
||||
const faults: Array<ValidationFault> = [];
|
||||
for (const entry of value) {
|
||||
const path = readString(entry, 'path');
|
||||
const message = readString(entry, 'message');
|
||||
if (path == null || message == null) continue;
|
||||
const code = readString(entry, 'code');
|
||||
faults.push(code == null ? {path, message} : {path, code, message});
|
||||
}
|
||||
return faults.length > 0 ? faults : undefined;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
|
||||
Reference in New Issue
Block a user