Skip to main content

Unable to fetch metadata

A permanently spinning “loading” state in the Fetch Metadata dialog usually means the backend request to the metadata provider failed or never returned. The container logs and your browser’s network tab will show which. Here’s how I’d narrow it down.

1. Check the container log while you search


bash
docker logs -f --tail 100 calibre-web

Run a search in the UI and watch for tracebacks. Typical hits are ConnectionError, Max retries exceeded, Temporary failure in name resolution, or a ModuleNotFoundError (a missing Python dependency in the image). If you want more detail, raise the log level under Admin → Edit Basic Configuration → Logfile Configuration → Log Level: Debug.

2. Test outbound connectivity from inside the container


bash
docker exec -it calibre-web sh -c 'python3 - <<EOF
import requests
for u in ["https://www.googleapis.com/books/v1/volumes?q=test",
          "https://comicvine.gamespot.com"]:
    try:
        print(u, requests.get(u, timeout=10).status_code)
    except Exception as e:
        print(u, "ERR", e)
EOF'

If this fails, the problem is Docker networking or DNS. Fixes include setting DNS in your compose file (dns: [1.1.1.1, 8.8.8.8]), checking the host firewall or iptables/ufw forwarding rules, and checking whether an IPv6-only resolution path is breaking things. An empty /etc/resolv.conf or a broken Docker bridge after a firewall change is a common cause on Ubuntu.

3. Check the browser side (F12 → Network)

When you click search, look for the POST to /metadata/search:

  • Pending forever or 504: a reverse proxy timeout or buffering issue. For nginx, make sure the proxy passes X-Forwarded-* headers and has a reasonable proxy_read_timeout.
  • 500: the backend error will be in the container log.
  • No request at all: a JavaScript error in the Console tab. Try a hard refresh or a private window, since a cached old JS file after an upgrade can break the dialog.

4. Provider-specific causes

  • Google Scholar is frequently blocked or rate-limited, because it returns captchas to servers. Untick it in the dialog.
  • Google Books can fail or be throttled from some IP ranges or without an API key. Try it alone.
  • ComicVine needs an API key configured.
  • Providers are toggled with the checkboxes in the Fetch Metadata dialog itself (admin account required). Try one provider at a time to see which one hangs the whole request.

5. Update the image

Metadata providers change often, and old versions break when upstream sites change.


bash
docker compose pull && docker compose up -d

If you use the linuxserver image, also check its release notes for known issues with the version you’re on.

If you paste the relevant log lines from step 1, plus how Calibre-Web is exposed (direct port or behind a reverse proxy), I can pinpoint it.


Your nginx config is the likely culprit, and it fits everything you’ve seen so far. Google, the other providers and the app log all look healthy, so the search request probably succeeds, and the browser then fails to render the results.

The suspect is this line in your CSP:


script-src 'self' 'unsafe-inline';

with the comment “No unsafe-eval required”. As far as I remember, Calibre-Web’s Fetch Metadata dialog (get_meta.js) builds its result rows with Underscore’s _.template, which compiles templates using new Function(). Browsers block that unless the CSP allows 'unsafe-eval'. The script then throws an error, and the “loading” message is never replaced. I’m going from memory here, so confirm it first.

1. Confirm in the browser

Open F12 → Console, run a search, and look for a red error like:


Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of the following Content Security Policy directive...

In the Network tab, metadata/search should show a plain 200 with JSON results. If both are true, this is the cause.

2. Fix it, only on the edit page

Your CSP applies to the whole site, so allow unsafe-eval only where the dialog lives (/admin/book/<id>). Add this map next to your existing $loggable map in the http context:


nginx
map $uri $csp_script_src {
    ~^/admin/book/  "'self' 'unsafe-inline' 'unsafe-eval'";
    default         "'self' 'unsafe-inline'";
}

Then change script-src in your CSP line to use the variable:


nginx
add_header Content-Security-Policy "default-src 'self'; script-src $csp_script_src; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; media-src 'self'; frame-src 'self'; object-src 'none'; base-uri 'self';" always;

Update the comment above it as well, since the “no unsafe-eval required” note is no longer accurate. Apply it:


bash
sudo nginx -t && sudo systemctl reload nginx

Hard-refresh the edit page (Ctrl+Shift+R) and retry Fetch Metadata.

3. Quick test without editing anything

To check the theory first, load the Calibre-Web port directly (http://SERVER_IP:8083, or over Tailscale if you use it). That bypasses nginx and its CSP. If the dialog works there, the CSP is confirmed.

Nothing else in the config looks wrong. The 300 s proxy_read_timeout is generous, so a proxy timeout isn’t a plausible cause. The static/ and cover/ locations define their own add_header lines, which drops the CSP and the other headers for those responses, but that doesn’t affect this problem.

If the Console shows a different error, paste it and I’ll take it from there.

jQuery.Deferred exception: call to Function() blocked by CSP template@https://lib.meson.in/static/js/libs/underscore-umd-min.js:6:15234 @https://lib.meson.in/static/js/get_meta.js:24:23 e@https://lib.meson.in/static/js/libs/jquery.min.js:2:30310 Deferred/then/l/</t<@https://lib.meson.in/static/js/libs/jquery.min.js:2:30612 undefined [jquery.min.js:2:31865](https://lib.meson.in/static/js/libs/jquery.min.js)
Content-Security-Policy: The page’s settings blocked a JavaScript eval (script-src) from being executed because it violates the following directive: “script-src 'self' 'unsafe-inline'” (Missing 'unsafe-eval') [underscore-umd-min.js:6:15234](https://lib.meson.in/static/js/libs/underscore-umd-min.js)
Uncaught EvalError: call to Function() blocked by CSP
template Underscore
<anonymous> https://lib.meson.in/static/js/get_meta.js:24
jQuery 2
[underscore-umd-min.js:6:15234](https://lib.meson.in/static/js/libs/underscore-umd-min.js)
MouseEvent.mozInputSource is deprecated. Use PointerEvent.pointerType instead.

Fixing nginx header configuration for the admin dialog route.

That’s the confirmation. get_meta.js calls Underscore’s _.template, which uses new Function(), and your CSP blocks it because script-src has no 'unsafe-eval'. The search itself is succeeding. The browser just refuses to render the results, so the dialog stays on “loading”.

1. Add the map in the http context, next to your existing $loggable map:


nginx
# Calibre-Web's Fetch Metadata dialog needs 'unsafe-eval' (Underscore templates)
map $uri $csp_script_src {
    ~^/admin/book/  "'self' 'unsafe-inline' 'unsafe-eval'";
    default         "'self' 'unsafe-inline'";
}

2. Use it in the CSP in the HTTPS server block, replacing the existing Content-Security-Policy line:


nginx
add_header Content-Security-Policy "default-src 'self'; script-src $csp_script_src; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; media-src 'self'; frame-src 'self'; object-src 'none'; base-uri 'self';" always;

Also change the comment above it, which still says “No unsafe-eval required”.

3. Test and reload:


bash
sudo nginx -t && sudo systemctl reload nginx

Hard-refresh the book edit page (Ctrl+Shift+R) and run Fetch Metadata again. The CSP is delivered with the HTML page, so it’s the edit page’s URL (/admin/book/<id>) that has to match the map.

Notes

  • The relaxation applies only to the book edit pages, which only admins or users with edit rights can reach, so the rest of the site keeps the stricter policy.
  • If you’d rather not use a map, you can simply add 'unsafe-eval' to script-src site-wide. That works, but it weakens the policy everywhere.
  • Your Google Books key is still useful: the 429s were real, and the key fixes them. You don’t need the timeout patch to google.py any more, so remove that bind mount if you added it.
  • If you want a different level of security, you could also remove the unsafe-inline from the policy later, but Calibre-Web depends on inline scripts, so I’d leave it.

If the dialog still hangs after the reload, check the Console for a new error and send it over.