Skip to content

fix(reverseproxy): set is_proxied=True when X-Forwarded-Proto is present#3595

Open
austin-rt wants to merge 2 commits intojaneczku:masterfrom
austin-rt:fix/cloudflare-tunnel-is-proxied
Open

fix(reverseproxy): set is_proxied=True when X-Forwarded-Proto is present#3595
austin-rt wants to merge 2 commits intojaneczku:masterfrom
austin-rt:fix/cloudflare-tunnel-is-proxied

Conversation

@austin-rt
Copy link
Copy Markdown

@austin-rt austin-rt commented Feb 24, 2026

Problem

When Calibre-Web is served through a Cloudflare Tunnel (or any reverse proxy that sends X-Forwarded-Proto without X-Forwarded-Host), the Kobo sync URL is generated with localhost:8083 instead of the public HTTPS URL.

Root cause: ReverseProxied.is_proxied only returns True when HTTP_X_FORWARDED_HOST is present. Cloudflare Tunnel (and most standard reverse proxy configurations including nginx, Caddy, and Traefik in their default configs) sends X-Forwarded-Proto but not X-Forwarded-Host, so is_proxied stays False. This causes kobo.py:get_download_url_for_book() to fall back to an explicit host:port URL, appending the internal container port (default 8083) — e.g. https://books.example.com:8083/kobo/.... This breaks Kobo wireless sync silently: books appear in the library but fail to download.

My Setup

  • Raspberry Pi running Calibre-Web in Docker
  • Cloudflare Tunnel exposing it externally (no port forwarding)
  • Kobo Clara BW syncing via the external URL

Changes

1. cps/reverseproxy.py — Core fix

Also check X-Forwarded-Proto when determining scheme, and set self.proxied = True whenever any proxy header is detected — consistent with the existing X-Forwarded-Host behaviour.

# Before
scheme = environ.get('HTTP_X_SCHEME', '')
if scheme:
    environ['wsgi.url_scheme'] = scheme

# After
scheme = environ.get('HTTP_X_SCHEME', '') or environ.get('HTTP_X_FORWARDED_PROTO', '')
if scheme:
    environ['wsgi.url_scheme'] = scheme
    self.proxied = True

2. cps/config_sql.py — Schema default change

Changed config_external_port column default from 8083 to None. The old default was misleading behind a proxy — it caused the port to always be appended to URLs even when it shouldn't be.

3. cps/kobo.py — Runtime fallback

Fall back to DEFAULT_PORT (8083) at runtime when config_external_port is None, preserving existing behavior for non-proxied setups.

4. cps/admin.py — Blank port handling + proxy context

  • Passes is_proxied to config_edit.html and admin.html templates
  • Handles blank External Port submission as NULL instead of crashing (int('') threw ValueError)

5. cps/templates/config_edit.html — Port field UX

  • Port field is no longer required; value is blank when NULL; placeholder shows 8083
  • Help text shown when proxy detected:

    "A reverse proxy was detected. Leave blank unless you need to override the port for direct access."

6. cps/templates/admin.html — Config display

  • Shows stored value if set
  • Shows "Not set — reverse proxy detected" (italic) if proxied and not set
  • Falls back to 8083 if not proxied and not set

Notes

  • ProxyFix in __init__.py already correctly handles X-Forwarded-Proto for scheme detection, and TRUSTED_PROXY_COUNT is documented for Cloudflare Tunnel. This closes the gap in is_proxied, which is a separate check used by kobo.py.
  • X-Forwarded-Proto is the most universally forwarded proxy header — this fix broadens is_proxied detection to cover Cloudflare Tunnel and the majority of standard proxy setups that don't explicitly set X-Forwarded-Host.
  • I've raised the same fix against Calibre-Web Automated: fix: set is_proxied=True when X-Forwarded-Proto header is present crocodilestick/Calibre-Web-Automated#1143.

Verified

Tested end-to-end on a fresh Docker install behind a Cloudflare Tunnel:

Test Result
Reverse proxy auto-detected via X-Forwarded-Proto
Help text "A reverse proxy was detected..." shown on config page
Port field blank by default with placeholder 8083
Config saves cleanly with blank port (previously crashed)
Kobo Auth URL uses public hostname (https://...)
Kobo download URLs use public hostname (not localhost:8083)
E2E tested on fresh Docker install with empty library
Kobo device sync confirmed — books downloaded to device (Kobo Clara BW)

Note: Python isn't my primary language so apologies if the implementation isn't idiomatic — happy to iterate based on feedback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Default config_external_port to NULL instead of 8083 so it doesn't
  mislead when a reverse proxy is in use
- Fall back to DEFAULT_PORT at runtime in kobo.py when port is NULL
- Handle blank port submission in admin.py (previously crashed with
  int('') ValueError)
- Pass is_proxied to config and admin templates
- Show placeholder 8083 and proxy help text on config form
- Show "Not set — reverse proxy detected" on admin display

Co-authored-by: Cursor <cursoragent@cursor.com>
@austin-rt
Copy link
Copy Markdown
Author

Status: All changes verified via automated e2e testing against a fresh Docker install behind a Cloudflare Tunnel — Kobo sync URLs correctly use the public hostname, config saves cleanly with a blank port field, and reverse proxy detection works.

Keeping this as a draft until I can confirm full end-to-end sync onto a physical Kobo device. Will mark ready for review once that's verified.

@austin-rt austin-rt marked this pull request as ready for review February 26, 2026 16:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant