fix: stop masking connection failures across media server sync and login (#3324)

This commit is contained in:
fallenbagel
2026-08-16 18:31:44 +05:00
committed by GitHub
parent c9f2ac58be
commit 4d17e08b91
12 changed files with 223 additions and 103 deletions
+74 -14
View File
@@ -233,7 +233,11 @@ class JellyfinAPI extends ExternalAPI {
}
);
throw new ApiError(e.response?.status, ApiErrorCode.Unknown);
if (!e.response) {
throw new ApiError(502, ApiErrorCode.ConnectionError);
}
throw new ApiError(e.response.status, ApiErrorCode.Unknown);
}
}
@@ -250,7 +254,11 @@ class JellyfinAPI extends ExternalAPI {
{ label: 'Jellyfin API', error: e.response?.status }
);
throw new ApiError(e.response?.status, ApiErrorCode.Unknown);
if (!e.response) {
throw new ApiError(502, ApiErrorCode.ConnectionError);
}
throw new ApiError(e.response.status, ApiErrorCode.Unknown);
}
}
@@ -270,7 +278,11 @@ class JellyfinAPI extends ExternalAPI {
{ label: 'Jellyfin API', error: e.response?.status }
);
throw new ApiError(e.response?.status, ApiErrorCode.Unknown);
if (!e.response) {
throw new ApiError(502, ApiErrorCode.ConnectionError);
}
throw new ApiError(e.response.status, ApiErrorCode.Unknown);
}
}
@@ -289,7 +301,11 @@ class JellyfinAPI extends ExternalAPI {
{ label: 'Jellyfin API', error: e.response?.status }
);
throw new ApiError(e.response?.status, ApiErrorCode.Unknown);
if (!e.response) {
throw new ApiError(502, ApiErrorCode.ConnectionError);
}
throw new ApiError(e.response.status, ApiErrorCode.Unknown);
}
}
@@ -304,7 +320,11 @@ class JellyfinAPI extends ExternalAPI {
return systemInfoResponse;
} catch (e) {
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
if (!e.response) {
throw new ApiError(502, ApiErrorCode.ConnectionError);
}
throw new ApiError(e.response.status, ApiErrorCode.InvalidAuthToken);
}
}
@@ -321,7 +341,11 @@ class JellyfinAPI extends ExternalAPI {
{ label: 'Jellyfin API', error: e.response?.status }
);
throw new ApiError(e.response?.status, ApiErrorCode.Unknown);
if (!e.response) {
throw new ApiError(502, ApiErrorCode.ConnectionError);
}
throw new ApiError(e.response.status, ApiErrorCode.Unknown);
}
}
@@ -336,7 +360,11 @@ class JellyfinAPI extends ExternalAPI {
{ label: 'Jellyfin API', error: e.response?.status }
);
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
if (!e.response) {
throw new ApiError(502, ApiErrorCode.ConnectionError);
}
throw new ApiError(e.response.status, ApiErrorCode.InvalidAuthToken);
}
}
@@ -352,7 +380,11 @@ class JellyfinAPI extends ExternalAPI {
{ label: 'Jellyfin API', error: e.response?.status }
);
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
if (!e.response) {
throw new ApiError(502, ApiErrorCode.ConnectionError);
}
throw new ApiError(e.response.status, ApiErrorCode.InvalidAuthToken);
}
}
@@ -379,6 +411,10 @@ class JellyfinAPI extends ExternalAPI {
}
);
if (!e.response) {
throw new ApiError(502, ApiErrorCode.ConnectionError);
}
return [];
}
}
@@ -425,7 +461,11 @@ class JellyfinAPI extends ExternalAPI {
{ label: 'Jellyfin API', error: e?.response?.status }
);
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
if (!e.response) {
throw new ApiError(502, ApiErrorCode.ConnectionError);
}
throw new ApiError(e.response.status, ApiErrorCode.InvalidAuthToken);
}
}
@@ -450,7 +490,11 @@ class JellyfinAPI extends ExternalAPI {
{ label: 'Jellyfin API', error: e.response?.status }
);
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
if (!e.response) {
throw new ApiError(502, ApiErrorCode.ConnectionError);
}
throw new ApiError(e.response.status, ApiErrorCode.InvalidAuthToken);
}
}
@@ -477,7 +521,11 @@ class JellyfinAPI extends ExternalAPI {
`Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
{ label: 'Jellyfin API', error: e.response?.status }
);
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
if (!e.response) {
throw new ApiError(502, ApiErrorCode.ConnectionError);
}
throw new ApiError(e.response.status, ApiErrorCode.InvalidAuthToken);
}
}
@@ -492,7 +540,11 @@ class JellyfinAPI extends ExternalAPI {
{ label: 'Jellyfin API', error: e.response?.status }
);
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
if (!e.response) {
throw new ApiError(502, ApiErrorCode.ConnectionError);
}
throw new ApiError(e.response.status, ApiErrorCode.InvalidAuthToken);
}
}
@@ -523,7 +575,11 @@ class JellyfinAPI extends ExternalAPI {
{ label: 'Jellyfin API', error: e.response?.status }
);
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
if (!e.response) {
throw new ApiError(502, ApiErrorCode.ConnectionError);
}
throw new ApiError(e.response.status, ApiErrorCode.InvalidAuthToken);
}
}
@@ -540,7 +596,11 @@ class JellyfinAPI extends ExternalAPI {
{ label: 'Jellyfin API', error: e.response?.status }
);
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
if (!e.response) {
throw new ApiError(502, ApiErrorCode.ConnectionError);
}
throw new ApiError(e.response.status, ApiErrorCode.InvalidAuthToken);
}
}
}
+7 -1
View File
@@ -1,7 +1,9 @@
import ExternalAPI from '@server/api/externalapi';
import { ApiErrorCode } from '@server/constants/error';
import type { Library, PlexSettings } from '@server/lib/settings';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import { ApiError } from '@server/types/error';
interface PlexStatusResponse {
MediaContainer: {
@@ -167,7 +169,11 @@ class PlexAPI extends ExternalAPI {
message: e.message,
});
settings.plex.libraries = [];
if (!e.response) {
throw new ApiError(502, ApiErrorCode.ConnectionError);
}
throw new ApiError(e.response.status, ApiErrorCode.Unknown);
}
await settings.save();
+1
View File
@@ -5,6 +5,7 @@ export enum ApiErrorCode {
InvalidEmail = 'INVALID_EMAIL',
NotAdmin = 'NOT_ADMIN',
NoAdminUser = 'NO_ADMIN_USER',
ConnectionError = 'CONNECTION_ERROR',
SyncErrorGroupedFolders = 'SYNC_ERROR_GROUPED_FOLDERS',
SyncErrorNoLibraries = 'SYNC_ERROR_NO_LIBRARIES',
Unauthorized = 'UNAUTHORIZED',
+11 -1
View File
@@ -120,7 +120,17 @@ app
});
const plexapi = new PlexAPI({ plexToken: admin.plexToken });
await plexapi.syncLibraries();
try {
await plexapi.syncLibraries();
} catch {
// Leave the existing libraries untouched so the migration retries on
// the next startup instead of discarding the user's configuration
logger.warn(
'Failed to migrate Plex libraries; will retry on next startup',
{ label: 'Settings' }
);
}
}
}
+24
View File
@@ -543,6 +543,30 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
message: e.errorCode,
});
case ApiErrorCode.ConnectionError:
logger.error(
`Unable to reach the ${
settings.main.mediaServerType === MediaServerType.JELLYFIN
? ServerType.JELLYFIN
: ServerType.EMBY
} server.`,
{
label: 'Auth',
error: e.errorCode,
status: e.statusCode,
hostname: getHostname({
useSsl: body.useSsl,
ip: body.hostname,
port: body.port,
urlBase: body.urlBase,
}),
}
);
return next({
status: e.statusCode,
message: e.errorCode,
});
case ApiErrorCode.InvalidCredentials:
logger.warn(
'Failed sign-in attempt from user with incorrect Jellyfin credentials',
+32 -15
View File
@@ -1,3 +1,4 @@
import type { JellyfinLibrary } from '@server/api/jellyfin';
import JellyfinAPI from '@server/api/jellyfin';
import PlexAPI from '@server/api/plexapi';
import PlexTvAPI from '@server/api/plextv';
@@ -263,7 +264,7 @@ settingsRoutes.put('/plex/library/:libraryId', async (req, res, next) => {
return res.status(200).json(library);
});
settingsRoutes.post('/plex/library/sync', async (_req, res) => {
settingsRoutes.post('/plex/library/sync', async (_req, res, next) => {
const settings = getSettings();
const userRepository = getRepository(User);
@@ -273,7 +274,14 @@ settingsRoutes.post('/plex/library/sync', async (_req, res) => {
});
const plexapi = new PlexAPI({ plexToken: admin.plexToken });
await plexapi.syncLibraries();
try {
await plexapi.syncLibraries();
} catch (e) {
return next({
status: e.statusCode ?? 500,
message: e.errorCode ?? ApiErrorCode.Unknown,
});
}
return res.status(200).json(settings.plex.libraries);
});
@@ -400,22 +408,31 @@ settingsRoutes.post('/jellyfin/library/sync', async (_req, res, next) => {
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
const libraries = await jellyfinClient.getLibraries();
let libraries: JellyfinLibrary[];
if (libraries.length === 0) {
// Check if no libraries are found due to the fallback to user views
// This only affects LDAP users
const account = await jellyfinClient.getUser();
try {
libraries = await jellyfinClient.getLibraries();
// Automatic Library grouping is not supported when user views are used to get library
if (account.Configuration.GroupedFolders?.length > 0) {
return next({
status: 501,
message: ApiErrorCode.SyncErrorGroupedFolders,
});
if (libraries.length === 0) {
// Check if no libraries are found due to the fallback to user views
// This only affects LDAP users
const account = await jellyfinClient.getUser();
// Automatic Library grouping is not supported when user views are used to get library
if (account.Configuration.GroupedFolders?.length > 0) {
return next({
status: 501,
message: ApiErrorCode.SyncErrorGroupedFolders,
});
}
return next({ status: 404, message: ApiErrorCode.SyncErrorNoLibraries });
}
return next({ status: 404, message: ApiErrorCode.SyncErrorNoLibraries });
} catch (e) {
return next({
status: e.statusCode ?? 500,
message: e.errorCode ?? ApiErrorCode.Unknown,
});
}
const newLibraries: Library[] = libraries.map((library) => {
+1
View File
@@ -98,6 +98,7 @@ const JellyfinLogin = ({ revalidate, serverType }: JellyfinLoginProps) => {
let errorMessage = messages.loginerror;
switch (e?.response?.data?.message) {
case ApiErrorCode.InvalidUrl:
case ApiErrorCode.ConnectionError:
errorMessage = messages.invalidurlerror;
break;
case ApiErrorCode.InvalidCredentials:
+16 -7
View File
@@ -47,6 +47,8 @@ const messages = defineMessages('components.Settings', {
'Custom authentication with Automatic Library Grouping not supported',
jellyfinSyncFailedGenericError:
'Something went wrong while syncing libraries',
jellyfinSyncFailedConnectionError:
'Unable to reach the {mediaServerName} server. Check that it is running and reachable from Seerr.',
toggleLibraryFailure: 'Failed to update library.',
invalidurlerror: 'Unable to connect to {mediaServerName} server.',
syncing: 'Syncing',
@@ -86,11 +88,9 @@ interface SyncStatus {
interface SettingsJellyfinProps {
isSetupSettings?: boolean;
onComplete?: () => void;
}
const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
onComplete,
isSetupSettings,
}) => {
const [isSyncing, setIsSyncing] = useState(false);
@@ -102,7 +102,7 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
const { data: dataSync, mutate: revalidateSync } = useSWR<SyncStatus>(
'/api/v1/settings/jellyfin/sync',
{
refreshInterval: 1000,
refreshInterval: (latestData) => (latestData?.running ? 1000 : 10000),
}
);
const intl = useIntl();
@@ -177,6 +177,19 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
appearance: 'warning',
}
);
} else if (e?.response?.data?.message === 'CONNECTION_ERROR') {
addToast(
intl.formatMessage(messages.jellyfinSyncFailedConnectionError, {
mediaServerName:
settings.currentSettings.mediaServerType === MediaServerType.EMBY
? 'Emby'
: 'Jellyfin',
}),
{
autoDismiss: true,
appearance: 'error',
}
);
} else if (e?.response?.data?.message === 'SYNC_ERROR_NO_LIBRARIES') {
addToast(
intl.formatMessage(messages.jellyfinSyncFailedNoLibrariesFound),
@@ -216,10 +229,6 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
await axios.put(`/api/v1/settings/jellyfin/library/${libraryId}`, {
enabled: !activeLibraries.includes(libraryId),
});
if (onComplete) {
onComplete();
}
} catch {
addToast(intl.formatMessage(messages.toggleLibraryFailure), {
autoDismiss: true,
+30 -17
View File
@@ -42,6 +42,7 @@ const messages = defineMessages('components.Settings', {
toastPlexRefreshSuccess: 'Plex server list retrieved successfully!',
toastPlexRefreshFailure: 'Failed to retrieve Plex server list.',
toastPlexSyncFailure: 'Failed to sync Plex libraries.',
invalidurlerror: 'Unable to connect to {mediaServerName} server.',
toggleLibraryFailure: 'Failed to update library.',
toastPlexConnecting: 'Attempting to connect to Plex…',
toastPlexConnectingSuccess: 'Plex connection established successfully!',
@@ -110,10 +111,10 @@ interface PresetServerDisplay {
message?: string;
}
interface SettingsPlexProps {
onComplete?: () => void;
isSetupSettings?: boolean;
}
const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
const SettingsPlex = ({ isSetupSettings }: SettingsPlexProps) => {
const [isSyncing, setIsSyncing] = useState(false);
const [isRefreshingPresets, setIsRefreshingPresets] = useState(false);
const [availableServers, setAvailableServers] = useState<PlexDevice[] | null>(
@@ -124,12 +125,17 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
error,
mutate: revalidate,
} = useSWR<PlexSettings>('/api/v1/settings/plex');
const { data: dataTautulli, mutate: revalidateTautulli } =
useSWR<TautulliSettings>('/api/v1/settings/tautulli');
const {
data: dataTautulli,
error: errorTautulli,
mutate: revalidateTautulli,
} = useSWR<TautulliSettings>(
isSetupSettings ? null : '/api/v1/settings/tautulli'
);
const { data: dataSync, mutate: revalidateSync } = useSWR<SyncStatus>(
'/api/v1/settings/plex/sync',
{
refreshInterval: 1000,
refreshInterval: (latestData) => (latestData?.running ? 1000 : 10000),
}
);
const intl = useIntl();
@@ -244,11 +250,18 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
try {
await axios.post('/api/v1/settings/plex/library/sync');
} catch {
addToast(intl.formatMessage(messages.toastPlexSyncFailure), {
autoDismiss: true,
appearance: 'error',
});
} catch (e) {
addToast(
e?.response?.data?.message === 'CONNECTION_ERROR'
? intl.formatMessage(messages.invalidurlerror, {
mediaServerName: 'Plex',
})
: intl.formatMessage(messages.toastPlexSyncFailure),
{
autoDismiss: true,
appearance: 'error',
}
);
} finally {
setIsSyncing(false);
revalidate();
@@ -315,10 +328,6 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
await axios.put(`/api/v1/settings/plex/library/${libraryId}`, {
enabled: !activeLibraries.includes(libraryId),
});
if (onComplete) {
onComplete();
}
} catch {
addToast(intl.formatMessage(messages.toggleLibraryFailure), {
autoDismiss: true,
@@ -330,7 +339,11 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
}
};
if ((!data || !dataTautulli) && !error) {
if (
(!data || (!isSetupSettings && !dataTautulli)) &&
!error &&
!errorTautulli
) {
return <LoadingSpinner />;
}
return (
@@ -346,7 +359,7 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
<p className="description">
{intl.formatMessage(messages.plexsettingsDescription)}
</p>
{!!onComplete && (
{isSetupSettings && (
<div className="section">
<Alert
title={intl.formatMessage(messages.settingUpPlexDescription, {
@@ -730,7 +743,7 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
</div>
</div>
</div>
{!onComplete && (
{!isSetupSettings && (
<>
<div className="mb-6 mt-10">
<h3 className="heading">
+1
View File
@@ -133,6 +133,7 @@ function JellyfinSetup({
let errorMessage = messages.loginerror;
switch (e?.response?.data?.message) {
case ApiErrorCode.InvalidUrl:
case ApiErrorCode.ConnectionError:
errorMessage = messages.invalidurlerror;
break;
case ApiErrorCode.InvalidCredentials:
+24 -47
View File
@@ -2,6 +2,7 @@ import EmbyLogo from '@app/assets/services/emby.svg';
import JellyfinLogo from '@app/assets/services/jellyfin.svg';
import PlexLogo from '@app/assets/services/plex.svg';
import AppDataWarning from '@app/components/AppDataWarning';
import Alert from '@app/components/Common/Alert';
import Button from '@app/components/Common/Button';
import ImageFader from '@app/components/Common/ImageFader';
import PageTitle from '@app/components/Common/PageTitle';
@@ -12,14 +13,13 @@ import SettingsServices from '@app/components/Settings/SettingsServices';
import SetupSteps from '@app/components/Setup/SetupSteps';
import useLocale from '@app/hooks/useLocale';
import useSettings from '@app/hooks/useSettings';
import useToasts from '@app/hooks/useToasts';
import defineMessages from '@app/utils/defineMessages';
import { MediaServerType } from '@server/constants/server';
import type { Library } from '@server/lib/settings';
import axios from 'axios';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { useCallback, useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
import useSWR, { mutate } from 'swr';
import SetupLogin from './SetupLogin';
@@ -39,22 +39,19 @@ const messages = defineMessages('components.Setup', {
configuremediaserver: 'Configure Media Server',
configureservices: 'Configure Services',
librarieserror:
'Validation failed. Please toggle the libraries again to continue.',
'Unable to load libraries from your media server. Check that it is reachable and still configured correctly.',
});
const Setup = () => {
const intl = useIntl();
const [isUpdating, setIsUpdating] = useState(false);
const [currentStep, setCurrentStep] = useState(1);
const [mediaServerSettingsComplete, setMediaServerSettingsComplete] =
useState(false);
const [mediaServerType, setMediaServerType] = useState(
MediaServerType.NOT_CONFIGURED
);
const router = useRouter();
const { locale } = useLocale();
const settings = useSettings();
const toasts = useToasts();
const finishSetup = async () => {
setIsUpdating(true);
@@ -71,34 +68,21 @@ const Setup = () => {
}
};
const validateLibraries = useCallback(async () => {
try {
const endpointMap: Record<MediaServerType, string> = {
[MediaServerType.JELLYFIN]: '/api/v1/settings/jellyfin',
[MediaServerType.EMBY]: '/api/v1/settings/jellyfin',
[MediaServerType.PLEX]: '/api/v1/settings/plex',
[MediaServerType.NOT_CONFIGURED]: '',
};
const mediaServerSettingsEndpoint: Record<MediaServerType, string | null> = {
[MediaServerType.JELLYFIN]: '/api/v1/settings/jellyfin',
[MediaServerType.EMBY]: '/api/v1/settings/jellyfin',
[MediaServerType.PLEX]: '/api/v1/settings/plex',
[MediaServerType.NOT_CONFIGURED]: null,
};
const endpoint = endpointMap[mediaServerType];
if (!endpoint) return;
const { data: mediaServerSettings, error: mediaServerSettingsError } =
useSWR<{ libraries: Library[] }>(
currentStep === 3 ? mediaServerSettingsEndpoint[mediaServerType] : null
);
const response = await axios.get(endpoint);
const hasEnabledLibraries = response.data?.libraries?.some(
(library: Library) => library.enabled
);
setMediaServerSettingsComplete(hasEnabledLibraries);
} catch {
toasts.addToast(intl.formatMessage(messages.librarieserror), {
autoDismiss: true,
appearance: 'error',
});
setMediaServerSettingsComplete(false);
}
}, [intl, mediaServerType, toasts]);
const mediaServerSettingsComplete = !!mediaServerSettings?.libraries?.some(
(library) => library.enabled
);
const { data: backdrops } = useSWR<string[]>('/api/v1/backdrops', {
refreshInterval: 0,
@@ -124,24 +108,11 @@ const Setup = () => {
settings.currentSettings.mediaServerType,
settings.currentSettings.initialized,
router,
toasts,
intl,
currentStep,
mediaServerType,
validateLibraries,
]);
useEffect(() => {
if (currentStep === 3) {
validateLibraries();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentStep]);
const handleComplete = () => {
validateLibraries();
};
if (settings.currentSettings.initialized) return <></>;
return (
@@ -266,10 +237,16 @@ const Setup = () => {
)}
{currentStep === 3 && (
<div className="p-2">
{!!mediaServerSettingsError && (
<Alert
title={intl.formatMessage(messages.librarieserror)}
type="error"
/>
)}
{mediaServerType === MediaServerType.PLEX ? (
<SettingsPlex onComplete={handleComplete} />
<SettingsPlex isSetupSettings />
) : (
<SettingsJellyfin isSetupSettings onComplete={handleComplete} />
<SettingsJellyfin isSetupSettings />
)}
<div className="actions">
<div className="flex justify-end">
+2 -1
View File
@@ -1215,6 +1215,7 @@
"components.Settings.jellyfinSettingsFailure": "Something went wrong while saving {mediaServerName} settings.",
"components.Settings.jellyfinSettingsSuccess": "{mediaServerName} settings saved successfully!",
"components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "Custom authentication with Automatic Library Grouping not supported",
"components.Settings.jellyfinSyncFailedConnectionError": "Unable to reach the {mediaServerName} server. Check that it is running and reachable from Seerr.",
"components.Settings.jellyfinSyncFailedGenericError": "Something went wrong while syncing libraries",
"components.Settings.jellyfinSyncFailedNoLibrariesFound": "No libraries were found",
"components.Settings.jellyfinlibraries": "{mediaServerName} Libraries",
@@ -1330,7 +1331,7 @@
"components.Setup.continue": "Continue",
"components.Setup.finish": "Finish Setup",
"components.Setup.finishing": "Finishing…",
"components.Setup.librarieserror": "Validation failed. Please toggle the libraries again to continue.",
"components.Setup.librarieserror": "Unable to load libraries from your media server. Check that it is reachable and still configured correctly.",
"components.Setup.servertype": "Choose Server Type",
"components.Setup.setup": "Setup",
"components.Setup.signin": "Sign In",