Skip to content

Jellyfin Web UI Subtitle Search Timeout Issue #7438

@enoch85

Description

@enoch85

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:

  1. User clicks search button
  2. Loading spinner appears
  3. Request is sent to the Jellyfin server
  4. Server queries subtitle providers (which can take 1-2+ minutes for providers like Bazarr that query multiple sources)
  5. 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:

  1. Query multiple upstream subtitle providers sequentially or in parallel
  2. Wait for responses from slow providers
  3. 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 300

2. 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

  1. Configure a subtitle provider that takes >60 seconds to respond
  2. Set up a reverse proxy with 30-second timeout
  3. Search for subtitles
  4. Before fix: Infinite spinner
  5. 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 endpoint
    • SubtitleManager.SearchSubtitles() - Provider orchestration
    • ISubtitleProvider.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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions