feat: better error handling for oidc api key

This also ships with a better error page
This commit is contained in:
Aarnav Tale
2025-11-28 17:54:33 -05:00
parent c0c4ecf631
commit d3d7c7cc0e
16 changed files with 476 additions and 256 deletions
+61 -112
View File
@@ -1,123 +1,72 @@
import { data } from 'react-router';
import { errors } from 'undici';
/**
* Helper function that determines if an error is a Node.js exception
* @param - The error to check
* @returns True if the error is a Node.js exception, false otherwise
*/
function isNodeNetworkError(error: unknown): error is NodeJS.ErrnoException {
if (typeof error !== 'object' || error === null) {
return false;
}
const keys = Object.keys(error as Record<string, unknown>);
return keys.includes('code') && keys.includes('errno');
return (
error != null &&
typeof error === 'object' &&
'code' in error &&
'errno' in error
);
}
export function friendlyError(givenError: unknown) {
let error: unknown = givenError;
/**
* A friendly error representation for Headscale connection issues.
*/
export interface HeadscaleConnectionError {
requestUrl: string;
errorCode: string;
errorMessage: string;
extraData: Record<string, unknown> | null;
}
/**
* Convert an Undici error into a friendly HeadscaleAPIError.
* This is used to avoid exposing rough error edges to the user.
*
* @param error - The Undici error to convert.
* @param requestUrl - The URL of the request that caused the error.
* @returns A friendly HeadscaleAPIError.
*/
export function undiciToFriendlyError(
error: unknown,
requestUrl: string,
): HeadscaleConnectionError {
// MARK: Do we need to go deeper into causes here?
if (error instanceof AggregateError) {
error = error.errors[0];
}
switch (true) {
case error instanceof errors.BodyTimeoutError:
case error instanceof errors.ConnectTimeoutError:
case error instanceof errors.HeadersTimeoutError:
return data('Timed out waiting for a response from the Headscale API', {
statusText: 'Request Timeout',
status: 408,
});
case error instanceof errors.SocketError:
case error instanceof errors.SecureProxyConnectionError:
case error instanceof errors.ClientClosedError:
case error instanceof errors.ClientDestroyedError:
case error instanceof errors.RequestAbortedError:
return data('The Headscale API is not reachable', {
statusText: 'Service Unavailable',
status: 503,
});
case error instanceof errors.InvalidArgumentError:
case error instanceof errors.InvalidReturnValueError:
case error instanceof errors.NotSupportedError:
return data('Unable to make a request (this is most likely a bug)', {
statusText: 'Internal Server Error',
status: 500,
});
case error instanceof errors.HeadersOverflowError:
case error instanceof errors.RequestContentLengthMismatchError:
case error instanceof errors.ResponseContentLengthMismatchError:
case error instanceof errors.ResponseExceededMaxSizeError:
return data('The Headscale API returned a malformed response', {
statusText: 'Bad Gateway',
status: 502,
});
case isNodeNetworkError(error):
if (error.code === 'ECONNREFUSED') {
return data('The Headscale API is not reachable', {
statusText: 'Service Unavailable',
status: 503,
});
}
if (error.code === 'ENOTFOUND') {
return data('The Headscale API is not reachable', {
statusText: 'Service Unavailable',
status: 503,
});
}
if (error.code === 'EAI_AGAIN') {
return data('The Headscale API is not reachable', {
statusText: 'Service Unavailable',
status: 503,
});
}
if (error.code === 'ETIMEDOUT') {
return data('Timed out waiting for a response from the Headscale API', {
statusText: 'Request Timeout',
status: 408,
});
}
if (error.code === 'ECONNRESET') {
return data('The Headscale API is not reachable', {
statusText: 'Service Unavailable',
status: 503,
});
}
if (error.code === 'EPIPE') {
return data('The Headscale API is not reachable', {
statusText: 'Service Unavailable',
status: 503,
});
}
if (error.code === 'ENETUNREACH') {
return data('The Headscale API is not reachable', {
statusText: 'Service Unavailable',
status: 503,
});
}
if (error.code === 'ENETRESET') {
return data('The Headscale API is not reachable', {
statusText: 'Service Unavailable',
status: 503,
});
}
return data('The Headscale API is not reachable', {
statusText: 'Service Unavailable',
status: 503,
});
default:
return data((error as Error).message ?? 'An unknown error occurred', {
statusText: 'Internal Server Error',
status: 500,
});
if (error instanceof errors.UndiciError) {
return {
requestUrl,
errorCode: error.code,
errorMessage: error.message,
extraData: null,
};
}
if (isNodeNetworkError(error)) {
return {
requestUrl,
errorCode: error.code ?? 'UNKNOWN_NODE_NETWORK_ERROR',
errorMessage: error.message,
extraData: {
syscall: error.syscall,
path: error.path,
errno: error.errno,
},
};
}
return {
requestUrl,
errorCode: 'UNKNOWN_ERROR',
errorMessage: 'An unknown error occured',
extraData: null,
};
}