Skip to content

Commit 5b4326b

Browse files
committed
Fix Jellyfin fetch failing due to private CA certificate
The pod couldn't verify tv.will.net's TLS cert (signed by Homelab CA, not in Node's default bundle). All server-side Jellyfin calls now use internalFetch which disables certificate verification via undici Agent. Also improves error messages by extracting e.cause details (ECONNREFUSED, ENOTFOUND) instead of passing through the opaque "fetch failed" string.
1 parent 2f3986e commit 5b4326b

5 files changed

Lines changed: 51 additions & 8 deletions

File tree

src/lib/server/services/cleanup.service.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { setTimeout as delay } from 'timers/promises';
55
import { basename } from 'path';
66
import { libraryService } from './library.service';
77
import { sseEmitter } from '../sse/emitter';
8+
import { internalFetch } from '../utils/fetch';
89

910
class CleanupService {
1011
private running = false;
@@ -115,7 +116,7 @@ class CleanupService {
115116
filepath: string,
116117
): Promise<string | null> {
117118
const searchTerm = filename.replace(/\.[^.]+$/, '');
118-
const res = await fetch(
119+
const res = await internalFetch(
119120
`${baseUrl}/Items?searchTerm=${encodeURIComponent(searchTerm)}&Recursive=true&Fields=Path&Limit=25`,
120121
{
121122
headers: { 'X-Emby-Token': apiKey },
@@ -144,7 +145,7 @@ class CleanupService {
144145
userIds: string[],
145146
): Promise<boolean> {
146147
for (const userId of userIds) {
147-
const res = await fetch(
148+
const res = await internalFetch(
148149
`${baseUrl}/Users/${userId}/Items/${itemId}/UserData`,
149150
{
150151
headers: { 'X-Emby-Token': apiKey },
@@ -177,7 +178,7 @@ class CleanupService {
177178
}
178179

179180
try {
180-
const res = await fetch(`${baseUrl}/Items/${jellyfinItemId}`, {
181+
const res = await internalFetch(`${baseUrl}/Items/${jellyfinItemId}`, {
181182
method: 'DELETE',
182183
headers: { 'X-Emby-Token': apiKey },
183184
signal: AbortSignal.timeout(10000),

src/lib/server/services/library.service.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { musicMetadataService } from './music-metadata.service';
77
import { ytdlpService } from './ytdlp.service';
88
import { plexService } from './plex.service';
99
import { effectiveCacheQuota } from '../permissions';
10+
import { internalFetch } from '../utils/fetch';
1011

1112
function sanitizeFilename(name: string): string {
1213
return name.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_').trim() || 'Unknown';
@@ -585,7 +586,7 @@ class LibraryService {
585586
if (settings.jellyfinUrl && settings.jellyfinApiKey) {
586587
try {
587588
const url = `${settings.jellyfinUrl.replace(/\/$/, '')}/Library/Refresh`;
588-
await fetch(url, {
589+
await internalFetch(url, {
589590
method: 'POST',
590591
headers: {
591592
'X-Emby-Token': settings.jellyfinApiKey,

src/lib/server/utils/fetch.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Agent } from 'undici';
2+
3+
const insecureAgent = new Agent({ connect: { rejectUnauthorized: false } });
4+
5+
/**
6+
* fetch() variant for internal/homelab services that may use self-signed or
7+
* private-CA certificates not trusted by Node's default CA bundle.
8+
*/
9+
export function internalFetch(url: string | URL, init?: RequestInit): Promise<Response> {
10+
return fetch(url, { ...init, dispatcher: insecureAgent } as RequestInit & { dispatcher: unknown });
11+
}

src/routes/api/settings/jellyfin-test/+server.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { json, error } from '@sveltejs/kit';
22
import { apiRoute } from '$lib/server/openapi';
3+
import { internalFetch } from '$lib/server/utils/fetch';
34
import type { RequestHandler } from './$types';
45

56
export const POST = apiRoute('/api/settings/jellyfin-test', 'POST', {
@@ -36,7 +37,7 @@ export const POST = apiRoute('/api/settings/jellyfin-test', 'POST', {
3637

3738
try {
3839
const baseUrl = url.replace(/\/$/, '');
39-
const res = await fetch(`${baseUrl}/System/Info`, {
40+
const res = await internalFetch(`${baseUrl}/System/Info`, {
4041
headers: { 'X-Emby-Token': apiKey },
4142
signal: AbortSignal.timeout(10000),
4243
});
@@ -48,7 +49,21 @@ export const POST = apiRoute('/api/settings/jellyfin-test', 'POST', {
4849
const info = await res.json();
4950
return json({ success: true, serverName: info.ServerName || 'Jellyfin' });
5051
} catch (e: any) {
51-
const message = e.name === 'TimeoutError' ? 'Connection timed out' : (e.message || 'Connection failed');
52+
let message: string;
53+
if (e.name === 'TimeoutError') {
54+
message = 'Connection timed out';
55+
} else if (e.cause) {
56+
const cause = e.cause as any;
57+
if (cause.code === 'ECONNREFUSED') {
58+
message = 'Connection refused — is Jellyfin running?';
59+
} else if (cause.code === 'ENOTFOUND') {
60+
message = `Could not resolve host — check the server URL`;
61+
} else {
62+
message = cause.message || 'Connection failed';
63+
}
64+
} else {
65+
message = e.message || 'Connection failed';
66+
}
5267
return json({ success: false, error: message });
5368
}
5469
}) satisfies RequestHandler;

src/routes/api/settings/jellyfin-users/+server.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { json, error } from '@sveltejs/kit';
22
import { prisma } from '$lib/server/db';
33
import { apiRoute } from '$lib/server/openapi';
4+
import { internalFetch } from '$lib/server/utils/fetch';
45
import type { RequestHandler } from './$types';
56

67
export const GET = apiRoute('/api/settings/jellyfin-users', 'GET', {
@@ -38,7 +39,7 @@ export const GET = apiRoute('/api/settings/jellyfin-users', 'GET', {
3839

3940
try {
4041
const baseUrl = settings.jellyfinUrl.replace(/\/$/, '');
41-
const res = await fetch(`${baseUrl}/Users`, {
42+
const res = await internalFetch(`${baseUrl}/Users`, {
4243
headers: { 'X-Emby-Token': settings.jellyfinApiKey },
4344
signal: AbortSignal.timeout(10000),
4445
});
@@ -53,7 +54,21 @@ export const GET = apiRoute('/api/settings/jellyfin-users', 'GET', {
5354
);
5455
} catch (e: any) {
5556
if (e.status) throw e;
56-
const message = e.name === 'TimeoutError' ? 'Connection timed out' : (e.message || 'Failed to fetch users');
57+
let message: string;
58+
if (e.name === 'TimeoutError') {
59+
message = 'Connection timed out';
60+
} else if (e.cause) {
61+
const cause = e.cause as any;
62+
if (cause.code === 'ECONNREFUSED') {
63+
message = 'Connection refused — is Jellyfin running?';
64+
} else if (cause.code === 'ENOTFOUND') {
65+
message = `Could not resolve host — check the server URL`;
66+
} else {
67+
message = cause.message || 'Failed to connect to Jellyfin';
68+
}
69+
} else {
70+
message = e.message || 'Failed to fetch users';
71+
}
5772
throw error(502, message);
5873
}
5974
}) satisfies RequestHandler;

0 commit comments

Comments
 (0)