feat: improve error returning and parsing logic

This commit is contained in:
Aarnav Tale 2025-04-03 03:00:38 -04:00
parent 234020eec5
commit 6a94e815f2
7 changed files with 56 additions and 38 deletions

View File

@ -1,16 +1,36 @@
import { AlertIcon } from '@primer/octicons-react'; import { AlertIcon } from '@primer/octicons-react';
import { isRouteErrorResponse, useRouteError } from 'react-router'; import { isRouteErrorResponse, useRouteError } from 'react-router';
import ResponseError from '~/server/headscale/api-error';
import cn from '~/utils/cn'; import cn from '~/utils/cn';
import Card from './Card'; import Card from './Card';
import Code from './Code';
interface Props { interface Props {
type?: 'full' | 'embedded'; type?: 'full' | 'embedded';
} }
function getMessage(error: Error | unknown) { function getMessage(error: Error | unknown): {
title: string;
message: string;
} {
if (error instanceof ResponseError) {
if (error.responseObject?.message) {
return {
title: 'Headscale Error',
message: String(error.responseObject.message),
};
}
return {
title: 'Headscale Error',
message: error.response,
};
}
if (!(error instanceof Error)) { if (!(error instanceof Error)) {
return 'An unknown error occurred'; return {
title: 'Unknown Error',
message: String(error),
};
} }
let rootError = error; let rootError = error;
@ -25,16 +45,22 @@ function getMessage(error: Error | unknown) {
// If we are aggregate, concat into a single message // If we are aggregate, concat into a single message
if (rootError instanceof AggregateError) { if (rootError instanceof AggregateError) {
return rootError.errors.map((error) => error.message).join('\n'); return {
title: 'Errors',
message: rootError.errors.map((error) => error.message).join('\n'),
};
} }
return rootError.message; return {
title: 'Error',
message: rootError.message,
};
} }
export function ErrorPopup({ type = 'full' }: Props) { export function ErrorPopup({ type = 'full' }: Props) {
const error = useRouteError(); const error = useRouteError();
const routing = isRouteErrorResponse(error); const routing = isRouteErrorResponse(error);
const message = getMessage(error); const { title, message } = getMessage(error);
return ( return (
<div <div
@ -48,12 +74,14 @@ export function ErrorPopup({ type = 'full' }: Props) {
<Card> <Card>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Card.Title className="text-3xl mb-0"> <Card.Title className="text-3xl mb-0">
{routing ? error.status : 'Error'} {routing ? error.status : title}
</Card.Title> </Card.Title>
<AlertIcon className="w-12 h-12 text-red-500" /> <AlertIcon className="w-12 h-12 text-red-500" />
</div> </div>
<Card.Text className="mt-4 text-lg"> <Card.Text
{routing ? error.statusText : <Code>{message}</Code>} className={cn('mt-4 text-lg', routing ? 'font-normal' : 'font-mono')}
>
{routing ? error.data.message : message}
</Card.Text> </Card.Text>
</Card> </Card>
</div> </div>

View File

@ -3,7 +3,7 @@ import { type LoaderFunctionArgs, redirect } from 'react-router';
import { Outlet, useLoaderData } from 'react-router'; import { Outlet, useLoaderData } from 'react-router';
import { ErrorPopup } from '~/components/Error'; import { ErrorPopup } from '~/components/Error';
import type { LoadContext } from '~/server'; import type { LoadContext } from '~/server';
import { ResponseError } from '~/server/headscale/api-client'; import ResponseError from '~/server/headscale/api-error';
import cn from '~/utils/cn'; import cn from '~/utils/cn';
import log from '~/utils/log'; import log from '~/utils/log';

View File

@ -1,4 +1,4 @@
import { BanIcon, CircleCheckIcon } from 'lucide-react'; import { CircleCheckIcon } from 'lucide-react';
import { import {
LoaderFunctionArgs, LoaderFunctionArgs,
Outlet, Outlet,

View File

@ -23,7 +23,7 @@ export default [
]), ]),
route('/users', 'routes/users/overview.tsx'), route('/users', 'routes/users/overview.tsx'),
route('/acls', 'routes/acls/editor.tsx'), route('/acls', 'routes/acls/overview.tsx'),
route('/dns', 'routes/dns/overview.tsx'), route('/dns', 'routes/dns/overview.tsx'),
...prefix('/settings', [ ...prefix('/settings', [

View File

@ -1,6 +1,7 @@
import { readFile } from 'node:fs/promises'; import { readFile } from 'node:fs/promises';
import { Agent, Dispatcher, request } from 'undici'; import { Agent, Dispatcher, request } from 'undici';
import log from '~/utils/log'; import log from '~/utils/log';
import ResponseError from './api-error';
export async function createApiClient(base: string, certPath?: string) { export async function createApiClient(base: string, certPath?: string) {
if (!certPath) { if (!certPath) {
@ -20,20 +21,6 @@ export async function createApiClient(base: string, certPath?: string) {
} }
} }
// Represents an error that occurred during a response
// Thrown when status codes are >= 400
export class ResponseError extends Error {
status: number;
response: string;
constructor(status: number, response: string) {
super(`Response Error (${status}): ${response}`);
this.name = 'ResponseError';
this.status = status;
this.response = response;
}
}
export class ApiClient { export class ApiClient {
private agent: Agent; private agent: Agent;
private base: string; private base: string;

View File

@ -3,10 +3,10 @@ export const Capabilities = {
// Can access the admin console // Can access the admin console
ui_access: 1 << 0, ui_access: 1 << 0,
// Read tailnet policy file // Read tailnet policy file (unimplemented)
read_policy: 1 << 1, read_policy: 1 << 1,
// Write tailnet policy file // Write tailnet policy file (unimplemented)
write_policy: 1 << 2, write_policy: 1 << 2,
// Read network configurations // Read network configurations
@ -16,13 +16,13 @@ export const Capabilities = {
// make subnet, or allow a node to be an exit node, enable HTTPS // make subnet, or allow a node to be an exit node, enable HTTPS
write_network: 1 << 4, write_network: 1 << 4,
// Read feature configuration // Read feature configuration (unimplemented)
read_feature: 1 << 5, read_feature: 1 << 5,
// Write feature configuration, for example, enable Taildrop // Write feature configuration, for example, enable Taildrop (unimplemented)
write_feature: 1 << 6, write_feature: 1 << 6,
// Configure user & group provisioning // Configure user & group provisioning (unimplemented)
configure_iam: 1 << 7, configure_iam: 1 << 7,
// Read machines, for example, see machine names and status // Read machines, for example, see machine names and status
@ -38,13 +38,13 @@ export const Capabilities = {
// approve users, make Admin // approve users, make Admin
write_users: 1 << 11, write_users: 1 << 11,
// Can generate authkeys // Can generate authkeys (unimplemented)
generate_authkeys: 1 << 12, generate_authkeys: 1 << 12,
// Can use any tag (without being tag owner) // Can use any tag (without being tag owner) (unimplemented)
use_tags: 1 << 13, use_tags: 1 << 13,
// Write tailnet name // Write tailnet name (unimplemented)
write_tailnet: 1 << 14, write_tailnet: 1 << 14,
// Owner flag // Owner flag

View File

@ -19,10 +19,13 @@ export function data400(message: string) {
} }
export function data403(message: string) { export function data403(message: string) {
return data({ return data(
success: false, {
message, success: false,
}); message,
},
{ status: 403 },
);
} }
export function data404(message: string) { export function data404(message: string) {