-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Description
This issue respects the following points:
- This issue is not already reported on GitHub (I've searched it).
- I agree to follow Jellyfin's Code of Conduct.
- This report addresses only a single issue; If you encounter multiple issues, kindly create separate reports for each one.
Describe the bug
The Jellyfin web client's subtitle search functionality lacks timeout handling and error recovery, causing an infinite loading spinner when subtitle provider searches take longer than typical reverse proxy timeouts (30-60 seconds).
Problem Description
When searching for subtitles through Jellyfin's native subtitle search UI:
- User clicks search button
- Loading spinner appears
- Request is sent to the Jellyfin server
- Server queries subtitle providers (which can take 1-2+ minutes for providers like Bazarr that query multiple sources)
- If a reverse proxy times out before the response arrives, the spinner never stops
User-Visible Symptoms
- Infinite spinning circle in the subtitle search dialog
- No error message displayed
- Search results never appear even though server logs show successful completion
- User must manually close/abort the dialog
Server-Side Evidence (Working Correctly)
[08:08:23] Searching subtitles for episode 3732 in language pt-br...
[08:09:31] Found 30 subtitles for episode 3732
[08:09:31] Episode subtitle search: 30 total subtitles, 3 after filtering for language 'pt-br'
The server successfully returns results after ~68 seconds, but the web client never receives them.
Root Cause Analysis
Affected File
jellyfin-web/src/components/subtitleeditor/subtitleeditor.js
Problematic Code (Lines 272-284)
function searchForSubtitles(context, language) {
userSettings.set('subtitleeditor-language', language);
loading.show();
const apiClient = ServerConnections.getApiClient(currentItem.ServerId);
const url = apiClient.getUrl('Items/' + currentItem.Id + '/RemoteSearch/Subtitles/' + language);
apiClient.getJSON(url).then(function (results) {
renderSearchResults(context, results);
});
}Issues Identified
1. No Timeout Configuration
The apiClient.getJSON() call uses the browser's native fetch() without a timeout parameter.
Looking at jellyfin-web/src/utils/fetch.js:
export function getFetchPromise(request) {
// ... setup code ...
if (!request.timeout) {
return fetch(url, fetchRequest); // No timeout - waits indefinitely!
}
return fetchWithTimeout(url, fetchRequest, request.timeout);
}Since no timeout is passed, the request will wait indefinitely until:
- The response arrives
- The connection is terminated externally (by reverse proxy, network, etc.)
2. No Error Handler
There is no .catch() block on the Promise chain:
apiClient.getJSON(url).then(function (results) {
renderSearchResults(context, results);
});
// Missing: .catch(function(error) { ... })If the Promise rejects (network error, timeout, etc.), nothing happens - no error message, no spinner removal.
3. Loading Spinner Never Hides on Error
loading.show() is called at the start, but loading.hide() only exists inside renderSearchResults():
function renderSearchResults(context, results) {
// ... render logic ...
loading.hide(); // Only called here!
}If renderSearchResults() is never called (due to error/timeout), the spinner stays forever.
Why This Manifests with Subtitle Providers
Subtitle providers like Bazarr, OpenSubtitles, etc. can take 1-2+ minutes to return results because they:
- Query multiple upstream subtitle providers sequentially or in parallel
- Wait for responses from slow providers
- Aggregate and deduplicate results
This exceeds typical reverse proxy timeouts:
- nginx default:
proxy_read_timeout 60s - Caddy default: 30 seconds
- Cloudflare: 100 seconds (varies by plan)
- AWS ALB: 60 seconds
Proposed Fix
Option A: Add Timeout and Error Handling (Recommended)
function searchForSubtitles(context, language) {
userSettings.set('subtitleeditor-language', language);
loading.show();
const apiClient = ServerConnections.getApiClient(currentItem.ServerId);
const url = apiClient.getUrl('Items/' + currentItem.Id + '/RemoteSearch/Subtitles/' + language);
// Use a longer timeout for subtitle searches (5 minutes)
// Subtitle providers can take 1-2+ minutes to query multiple sources
apiClient.ajax({
url: url,
type: 'GET',
dataType: 'json',
timeout: 300000 // 5 minutes in milliseconds
}).then(function (results) {
renderSearchResults(context, results);
}).catch(function (error) {
loading.hide();
// Show appropriate error message
if (error.name === 'AbortError' || error.message?.includes('timeout')) {
toast(globalize.translate('SubtitleSearchTimeout'));
} else {
toast(globalize.translate('SubtitleSearchError'));
}
// Clear results area and show no results message
context.querySelector('.subtitleResults').innerHTML = '';
context.querySelector('.noSearchResults').classList.remove('hide');
});
}Option B: Add AbortController for User Cancellation
Allow users to cancel long-running searches:
let currentSearchController = null;
function searchForSubtitles(context, language) {
// Cancel any existing search
if (currentSearchController) {
currentSearchController.abort();
}
currentSearchController = new AbortController();
userSettings.set('subtitleeditor-language', language);
loading.show();
const apiClient = ServerConnections.getApiClient(currentItem.ServerId);
const url = apiClient.getUrl('Items/' + currentItem.Id + '/RemoteSearch/Subtitles/' + language);
fetch(url, {
signal: currentSearchController.signal,
headers: apiClient.getDefaultHeaders()
})
.then(response => response.json())
.then(function (results) {
renderSearchResults(context, results);
})
.catch(function (error) {
if (error.name !== 'AbortError') {
loading.hide();
toast(globalize.translate('SubtitleSearchError'));
context.querySelector('.subtitleResults').innerHTML = '';
context.querySelector('.noSearchResults').classList.remove('hide');
}
})
.finally(function() {
currentSearchController = null;
});
}Required Localization Strings
Add to localization files:
{
"SubtitleSearchTimeout": "Subtitle search timed out. The provider may be slow - please try again.",
"SubtitleSearchError": "An error occurred while searching for subtitles."
}Workarounds for Users (Until Fixed)
1. Increase Reverse Proxy Timeout
nginx:
location / {
proxy_pass http://jellyfin:8096;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}Caddy:
reverse_proxy jellyfin:8096 {
transport http {
read_timeout 5m
}
}
Apache:
ProxyTimeout 3002. Access Jellyfin Directly
Bypass the reverse proxy by accessing Jellyfin directly on its port (e.g., http://server:8096) to confirm the issue is proxy-related.
Related Files in jellyfin-web
| File | Purpose |
|---|---|
src/components/subtitleeditor/subtitleeditor.js |
Main subtitle editor logic (contains the bug) |
src/components/subtitleeditor/subtitleeditor.template.html |
HTML template for subtitle dialog |
src/utils/fetch.js |
Fetch wrapper with timeout support |
src/components/loading/loading.js |
Loading spinner component |
Testing the Fix
- Configure a subtitle provider that takes >60 seconds to respond
- Set up a reverse proxy with 30-second timeout
- Search for subtitles
- Before fix: Infinite spinner
- After fix: Error message appears after timeout, spinner stops
References
- Jellyfin Web Repository: https://github.com/jellyfin/jellyfin-web
- Related code paths in Jellyfin server:
SubtitleController.SearchRemoteSubtitles()- API endpointSubtitleManager.SearchSubtitles()- Provider orchestrationISubtitleProvider.Search()- Provider interface
Reproduction Steps
See above
Expected/Actual behaviour
See above
Logs
No response
Server version
10.11.5
Web version
10.11.5
Build version
10.11.5
Platform
Linux (Ubuntu 24.04)
Browser
Firefox
Additional information
No response