mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2026-01-01 04:08:45 -05:00
fix(logging): handle media server connection refused error/toast (#748)
* fix(logging): handle media server connection refused error/toast Properly log as connection refused if the jellyfin/emby server is unreachable. Previously it used to throw a credentials error which lead to a lot of confusion * refactor(i8n): extract translation keys * refactor(auth): error message for a more consistent format * refactor(auth/errors): use custom error types and error codes instead of abusing error messages * refactor(i8n): replace connection refused translation key with invalidurl * fix(error): combine auth and api error class into a single one called network error * fix(error): use the new network error and network error codes in auth/api * refactor(error): rename NetworkError to ApiError
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { ApiErrorCode } from '@server/constants/error';
|
||||||
import availabilitySync from '@server/lib/availabilitySync';
|
import availabilitySync from '@server/lib/availabilitySync';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
|
import { ApiError } from '@server/types/error';
|
||||||
import type { AxiosInstance } from 'axios';
|
import type { AxiosInstance } from 'axios';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
@@ -129,9 +131,33 @@ class JellyfinAPI {
|
|||||||
Pw: Password,
|
Pw: Password,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return account.data;
|
return account.data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error('Unauthorized');
|
const status = e.response?.status;
|
||||||
|
|
||||||
|
const networkErrorCodes = new Set([
|
||||||
|
'ECONNREFUSED',
|
||||||
|
'EHOSTUNREACH',
|
||||||
|
'ENOTFOUND',
|
||||||
|
'ETIMEDOUT',
|
||||||
|
'ECONNRESET',
|
||||||
|
'EADDRINUSE',
|
||||||
|
'ENETDOWN',
|
||||||
|
'ENETUNREACH',
|
||||||
|
'EPIPE',
|
||||||
|
'ECONNABORTED',
|
||||||
|
'EPROTO',
|
||||||
|
'EHOSTDOWN',
|
||||||
|
'EAI_AGAIN',
|
||||||
|
'ERR_INVALID_URL',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (networkErrorCodes.has(e.code) || status === 404) {
|
||||||
|
throw new ApiError(status, ApiErrorCode.InvalidUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ApiError(status, ApiErrorCode.InvalidCredentials);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
5
server/constants/error.ts
Normal file
5
server/constants/error.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export enum ApiErrorCode {
|
||||||
|
InvalidUrl = 'INVALID_URL',
|
||||||
|
InvalidCredentials = 'INVALID_CREDENTIALS',
|
||||||
|
NotAdmin = 'NOT_ADMIN',
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import JellyfinAPI from '@server/api/jellyfin';
|
import JellyfinAPI from '@server/api/jellyfin';
|
||||||
import PlexTvAPI from '@server/api/plextv';
|
import PlexTvAPI from '@server/api/plextv';
|
||||||
|
import { ApiErrorCode } from '@server/constants/error';
|
||||||
import { MediaServerType } from '@server/constants/server';
|
import { MediaServerType } from '@server/constants/server';
|
||||||
import { UserType } from '@server/constants/user';
|
import { UserType } from '@server/constants/user';
|
||||||
import { getRepository } from '@server/datasource';
|
import { getRepository } from '@server/datasource';
|
||||||
@@ -9,6 +10,7 @@ import { Permission } from '@server/lib/permissions';
|
|||||||
import { getSettings } from '@server/lib/settings';
|
import { getSettings } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import { isAuthenticated } from '@server/middleware/auth';
|
import { isAuthenticated } from '@server/middleware/auth';
|
||||||
|
import { ApiError } from '@server/types/error';
|
||||||
import * as EmailValidator from 'email-validator';
|
import * as EmailValidator from 'email-validator';
|
||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import gravatarUrl from 'gravatar-url';
|
import gravatarUrl from 'gravatar-url';
|
||||||
@@ -278,7 +280,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
if (!user && !(await userRepository.count())) {
|
if (!user && !(await userRepository.count())) {
|
||||||
// Check if user is admin on jellyfin
|
// Check if user is admin on jellyfin
|
||||||
if (account.User.Policy.IsAdministrator === false) {
|
if (account.User.Policy.IsAdministrator === false) {
|
||||||
throw new Error('not_admin');
|
throw new ApiError(403, ApiErrorCode.NotAdmin);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -412,43 +414,63 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
|
|
||||||
return res.status(200).json(user?.filter() ?? {});
|
return res.status(200).json(user?.filter() ?? {});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message === 'Unauthorized') {
|
switch (e.errorCode) {
|
||||||
logger.warn(
|
case ApiErrorCode.InvalidUrl:
|
||||||
'Failed login attempt from user with incorrect Jellyfin credentials',
|
logger.error(
|
||||||
{
|
`The provided ${
|
||||||
label: 'Auth',
|
process.env.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin'
|
||||||
account: {
|
} is invalid or the server is not reachable.`,
|
||||||
ip: req.ip,
|
{
|
||||||
email: body.username,
|
label: 'Auth',
|
||||||
password: '__REDACTED__',
|
error: e.errorCode,
|
||||||
},
|
status: e.statusCode,
|
||||||
}
|
hostname: body.hostname,
|
||||||
);
|
}
|
||||||
return next({
|
);
|
||||||
status: 401,
|
return next({
|
||||||
message: 'Unauthorized',
|
status: e.statusCode,
|
||||||
});
|
message: e.errorCode,
|
||||||
} else if (e.message === 'not_admin') {
|
});
|
||||||
return next({
|
|
||||||
status: 403,
|
case ApiErrorCode.InvalidCredentials:
|
||||||
message: 'CREDENTIAL_ERROR_NOT_ADMIN',
|
logger.warn(
|
||||||
});
|
'Failed login attempt from user with incorrect Jellyfin credentials',
|
||||||
} else if (e.message === 'add_email') {
|
{
|
||||||
return next({
|
label: 'Auth',
|
||||||
status: 406,
|
account: {
|
||||||
message: 'CREDENTIAL_ERROR_ADD_EMAIL',
|
ip: req.ip,
|
||||||
});
|
email: body.username,
|
||||||
} else if (e.message === 'select_server_type') {
|
password: '__REDACTED__',
|
||||||
return next({
|
},
|
||||||
status: 406,
|
}
|
||||||
message: 'CREDENTIAL_ERROR_NO_SERVER_TYPE',
|
);
|
||||||
});
|
return next({
|
||||||
} else {
|
status: e.statusCode,
|
||||||
logger.error(e.message, { label: 'Auth' });
|
message: e.errorCode,
|
||||||
return next({
|
});
|
||||||
status: 500,
|
|
||||||
message: 'Something went wrong.',
|
case ApiErrorCode.NotAdmin:
|
||||||
});
|
logger.warn(
|
||||||
|
'Failed login attempt from user without admin permissions',
|
||||||
|
{
|
||||||
|
label: 'Auth',
|
||||||
|
account: {
|
||||||
|
ip: req.ip,
|
||||||
|
email: body.username,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return next({
|
||||||
|
status: e.statusCode,
|
||||||
|
message: e.errorCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
default:
|
||||||
|
logger.error(e.message, { label: 'Auth' });
|
||||||
|
return next({
|
||||||
|
status: 500,
|
||||||
|
message: 'Something went wrong.',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
9
server/types/error.ts
Normal file
9
server/types/error.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import type { ApiErrorCode } from '@server/constants/error';
|
||||||
|
|
||||||
|
export class ApiError extends Error {
|
||||||
|
constructor(public statusCode: number, public errorCode: ApiErrorCode) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.name = 'apiError';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import Button from '@app/components/Common/Button';
|
|||||||
import Tooltip from '@app/components/Common/Tooltip';
|
import Tooltip from '@app/components/Common/Tooltip';
|
||||||
import useSettings from '@app/hooks/useSettings';
|
import useSettings from '@app/hooks/useSettings';
|
||||||
import { InformationCircleIcon } from '@heroicons/react/24/solid';
|
import { InformationCircleIcon } from '@heroicons/react/24/solid';
|
||||||
|
import { ApiErrorCode } from '@server/constants/error';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Field, Form, Formik } from 'formik';
|
||||||
import getConfig from 'next/config';
|
import getConfig from 'next/config';
|
||||||
@@ -26,6 +27,7 @@ const messages = defineMessages({
|
|||||||
loginerror: 'Something went wrong while trying to sign in.',
|
loginerror: 'Something went wrong while trying to sign in.',
|
||||||
adminerror: 'You must use an admin account to sign in.',
|
adminerror: 'You must use an admin account to sign in.',
|
||||||
credentialerror: 'The username or password is incorrect.',
|
credentialerror: 'The username or password is incorrect.',
|
||||||
|
invalidurlerror: 'Unable to connect to {mediaServerName} server.',
|
||||||
signingin: 'Signing in…',
|
signingin: 'Signing in…',
|
||||||
signin: 'Sign In',
|
signin: 'Sign In',
|
||||||
initialsigningin: 'Connecting…',
|
initialsigningin: 'Connecting…',
|
||||||
@@ -91,14 +93,24 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
|||||||
email: values.email,
|
email: values.email,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
let errorMessage = null;
|
||||||
|
switch (e.response?.data?.message) {
|
||||||
|
case ApiErrorCode.InvalidUrl:
|
||||||
|
errorMessage = messages.invalidurlerror;
|
||||||
|
break;
|
||||||
|
case ApiErrorCode.InvalidCredentials:
|
||||||
|
errorMessage = messages.credentialerror;
|
||||||
|
break;
|
||||||
|
case ApiErrorCode.NotAdmin:
|
||||||
|
errorMessage = messages.adminerror;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
errorMessage = messages.loginerror;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
toasts.addToast(
|
toasts.addToast(
|
||||||
intl.formatMessage(
|
intl.formatMessage(errorMessage, mediaServerFormatValues),
|
||||||
e.message == 'Request failed with status code 401'
|
|
||||||
? messages.credentialerror
|
|
||||||
: e.message == 'Request failed with status code 403'
|
|
||||||
? messages.adminerror
|
|
||||||
: messages.loginerror
|
|
||||||
),
|
|
||||||
{
|
{
|
||||||
autoDismiss: true,
|
autoDismiss: true,
|
||||||
appearance: 'error',
|
appearance: 'error',
|
||||||
|
|||||||
@@ -219,8 +219,9 @@
|
|||||||
"components.Layout.VersionStatus.outofdate": "Out of Date",
|
"components.Layout.VersionStatus.outofdate": "Out of Date",
|
||||||
"components.Layout.VersionStatus.streamdevelop": "Jellyseerr Develop",
|
"components.Layout.VersionStatus.streamdevelop": "Jellyseerr Develop",
|
||||||
"components.Layout.VersionStatus.streamstable": "Jellyseerr Stable",
|
"components.Layout.VersionStatus.streamstable": "Jellyseerr Stable",
|
||||||
"components.Login.credentialerror": "The username or password is incorrect.",
|
|
||||||
"components.Login.adminerror": "You must use an admin account to sign in.",
|
"components.Login.adminerror": "You must use an admin account to sign in.",
|
||||||
|
"components.Login.invalidurlerror": "Unable to connect to {mediaServerName} server.",
|
||||||
|
"components.Login.credentialerror": "The username or password is incorrect.",
|
||||||
"components.Login.description": "Since this is your first time logging into {applicationName}, you are required to add a valid email address.",
|
"components.Login.description": "Since this is your first time logging into {applicationName}, you are required to add a valid email address.",
|
||||||
"components.Login.email": "Email Address",
|
"components.Login.email": "Email Address",
|
||||||
"components.Login.emailtooltip": "Address does not need to be associated with your {mediaServerName} instance.",
|
"components.Login.emailtooltip": "Address does not need to be associated with your {mediaServerName} instance.",
|
||||||
@@ -752,8 +753,8 @@
|
|||||||
"components.Settings.SettingsAbout.overseerrinformation": "About Jellyseerr",
|
"components.Settings.SettingsAbout.overseerrinformation": "About Jellyseerr",
|
||||||
"components.Settings.SettingsAbout.preferredmethod": "Preferred",
|
"components.Settings.SettingsAbout.preferredmethod": "Preferred",
|
||||||
"components.Settings.SettingsAbout.runningDevelop": "You are running the <code>develop</code> branch of Jellyseerr, which is only recommended for those contributing to development or assisting with bleeding-edge testing.",
|
"components.Settings.SettingsAbout.runningDevelop": "You are running the <code>develop</code> branch of Jellyseerr, which is only recommended for those contributing to development or assisting with bleeding-edge testing.",
|
||||||
"components.Settings.SettingsAbout.supportoverseerr": "Support Overseerr",
|
|
||||||
"components.Settings.SettingsAbout.supportjellyseerr": "Support Jellyseerr",
|
"components.Settings.SettingsAbout.supportjellyseerr": "Support Jellyseerr",
|
||||||
|
"components.Settings.SettingsAbout.supportoverseerr": "Support Overseerr",
|
||||||
"components.Settings.SettingsAbout.timezone": "Time Zone",
|
"components.Settings.SettingsAbout.timezone": "Time Zone",
|
||||||
"components.Settings.SettingsAbout.totalmedia": "Total Media",
|
"components.Settings.SettingsAbout.totalmedia": "Total Media",
|
||||||
"components.Settings.SettingsAbout.totalrequests": "Total Requests",
|
"components.Settings.SettingsAbout.totalrequests": "Total Requests",
|
||||||
@@ -938,17 +939,18 @@
|
|||||||
"components.Settings.hostname": "Hostname or IP Address",
|
"components.Settings.hostname": "Hostname or IP Address",
|
||||||
"components.Settings.internalUrl": "Internal URL",
|
"components.Settings.internalUrl": "Internal URL",
|
||||||
"components.Settings.is4k": "4K",
|
"components.Settings.is4k": "4K",
|
||||||
|
"components.Settings.jellyfinForgotPasswordUrl": "Forgot Password URL",
|
||||||
"components.Settings.jellyfinSettings": "{mediaServerName} Settings",
|
"components.Settings.jellyfinSettings": "{mediaServerName} Settings",
|
||||||
"components.Settings.jellyfinSettingsDescription": "Optionally configure the internal and external endpoints for your {mediaServerName} server. In most cases, the external URL is different to the internal URL. A custom password reset URL can also be set for {mediaServerName} login, in case you would like to redirect to a different password reset page.",
|
"components.Settings.jellyfinSettingsDescription": "Optionally configure the internal and external endpoints for your {mediaServerName} server. In most cases, the external URL is different to the internal URL. A custom password reset URL can also be set for {mediaServerName} login, in case you would like to redirect to a different password reset page.",
|
||||||
"components.Settings.jellyfinSettingsFailure": "Something went wrong while saving {mediaServerName} settings.",
|
"components.Settings.jellyfinSettingsFailure": "Something went wrong while saving {mediaServerName} settings.",
|
||||||
"components.Settings.jellyfinSettingsSuccess": "{mediaServerName} settings saved successfully!",
|
"components.Settings.jellyfinSettingsSuccess": "{mediaServerName} settings saved successfully!",
|
||||||
|
"components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "Custom authentication with Automatic Library Grouping not supported",
|
||||||
|
"components.Settings.jellyfinSyncFailedGenericError": "Something went wrong while syncing libraries",
|
||||||
|
"components.Settings.jellyfinSyncFailedNoLibrariesFound": "No libraries were found",
|
||||||
"components.Settings.jellyfinlibraries": "{mediaServerName} Libraries",
|
"components.Settings.jellyfinlibraries": "{mediaServerName} Libraries",
|
||||||
"components.Settings.jellyfinlibrariesDescription": "The libraries {mediaServerName} scans for titles. Click the button below if no libraries are listed.",
|
"components.Settings.jellyfinlibrariesDescription": "The libraries {mediaServerName} scans for titles. Click the button below if no libraries are listed.",
|
||||||
"components.Settings.jellyfinsettings": "{mediaServerName} Settings",
|
"components.Settings.jellyfinsettings": "{mediaServerName} Settings",
|
||||||
"components.Settings.jellyfinsettingsDescription": "Configure the settings for your {mediaServerName} server. {mediaServerName} scans your {mediaServerName} libraries to see what content is available.",
|
"components.Settings.jellyfinsettingsDescription": "Configure the settings for your {mediaServerName} server. {mediaServerName} scans your {mediaServerName} libraries to see what content is available.",
|
||||||
"components.Settings.jellyfinSyncFailedNoLibrariesFound": "No libraries were found",
|
|
||||||
"components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "Custom authentication with Automatic Library Grouping not supported",
|
|
||||||
"components.Settings.jellyfinSyncFailedGenericError": "Something went wrong while syncing libraries",
|
|
||||||
"components.Settings.librariesRemaining": "Libraries Remaining: {count}",
|
"components.Settings.librariesRemaining": "Libraries Remaining: {count}",
|
||||||
"components.Settings.manualscan": "Manual Library Scan",
|
"components.Settings.manualscan": "Manual Library Scan",
|
||||||
"components.Settings.manualscanDescription": "Normally, this will only be run once every 24 hours. Jellyseerr will check your Plex server's recently added more aggressively. If this is your first time configuring Plex, a one-time full manual library scan is recommended!",
|
"components.Settings.manualscanDescription": "Normally, this will only be run once every 24 hours. Jellyseerr will check your Plex server's recently added more aggressively. If this is your first time configuring Plex, a one-time full manual library scan is recommended!",
|
||||||
|
|||||||
Reference in New Issue
Block a user