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

View File

@ -3,7 +3,7 @@ import { type LoaderFunctionArgs, redirect } from 'react-router';
import { Outlet, useLoaderData } from 'react-router';
import { ErrorPopup } from '~/components/Error';
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 log from '~/utils/log';

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import { readFile } from 'node:fs/promises';
import { Agent, Dispatcher, request } from 'undici';
import log from '~/utils/log';
import ResponseError from './api-error';
export async function createApiClient(base: string, certPath?: string) {
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 {
private agent: Agent;
private base: string;

View File

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

View File

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