Skip to content

headless server can no longer be served behind proxyΒ #402

Closed

Description

@minrk

Describe the bug

πŸ’‘ Before filing, please check common issues:
Biorouter Troubleshooting Guide

πŸ“¦ To help us debug faster, attach a diagnostics bundle: in the app, Chat summary β†’ Diagnostics β†’ Generate diagnostics. It collects the transcript, a redacted copy of your configuration and the relevant logs. Read it before attaching β€” it contains the whole conversation.
πŸ‘‰ Full troubleshooting guide

A clear and concise description of what the bug is.


What previously worked (1.89.0):

biorouter-headless serve --port 1234 --public-url https://proxy.example.org/prefix/

where there is a reverse proxy forwarding the public url to biorouter-headless over localhost.

But the closest equivalent with 1.93:

biorouter serve --port 1234 --no-token

no longer works, for an apparent variety of reasons:

  • --public-url is removed, and no equivalent apparently presented
  • securely proxied https requests are rejected erroneously with the message
Open Biorouter through localhost or HTTPS. Use an SSH tunnel or a TLS proxy and a current browser that sends Fetch Metadata.

where the suggested solution is in fact exactly what is happening.

It seems biorouter is doing too much to attempt to verify secure connections, and getting it wrong.

To Reproduce
Steps to reproduce the behavior:

  1. start biorouter serve --port 9999 --no-token
  2. Access via HTTPS revese proxy
  3. See error above

Expected behavior
A clear and concise description of what you expected to happen.

The existing proxy still works:

  • trusted connection over localhost (or local BSD socket, if preferred)
  • proxied HTTPS requests
  • proxy with path prefix
  • it should not be biorouter's responsibility to check and enforce these things, other applications do not do this

Suspected cause
If you have an idea where the problem is (a file, a setting, a step that behaves differently from the documentation), describe it here. Optional.

serve appears to have been rewritten, and a number of features necessary for proxying to continue to function removed.


Screenshots
If applicable, add screenshots to help explain your problem.


Please provide the following information

  • OS & Arch: Debian 13, amd64
  • Interface: [UI / CLI] headless
  • Version: [e.g. v1.0.2] v1.93.0
  • Extensions enabled: doesn't get that far
  • Provider & Model: doesn't get that far

Additional context
Add any other context about the problem here.

Activity

  1. minrk commented on Oct 16, 2026

    @minrk
    Author

    fwiw, I can bypass the erroneous security message by setting the Host header to localhost in the proxy, but then we are back in the situation that the fix for #9 has still been lost by the removal of biorouter-headless

  2. Broccolito commented on Oct 17, 2026

    @Broccolito
    Collaborator

    Thanks for the report. You were right on every point, and this is fixed in #404 (merged to main; it ships in the next release).

    What was wrong in 1.93.0

    • The HTTPS refusal was a false positive. The check also required X-Forwarded-Proto: https and read its last value. Behind JupyterHub that header arrives as https,http, because configurable-http-proxy appends its own scheme. So a request your browser had already marked with Sec-Fetch-* headers was refused. The check now relies on the browser's Fetch Metadata, which browsers only send to https or localhost addresses, and ignores the proxy's scheme header. Plain-HTTP network access is still refused, because there the browser can't protect the page.
    • --public-url and path-prefix support (cannot proxy biorouter-headless on a path prefixΒ #9) were lost when biorouter-headless was retired. Both are back. The bundle now uses relative URLs and the daemon writes a <base href>, so a prefix the proxy strips works without rewriting anything.
    • Built apps had the same two problems. Their WebSocket was refused behind any HTTPS proxy, because Chrome sends no Fetch Metadata on WebSocket handshakes, and their URLs assumed /. Both are fixed.

    Behind a proxy

    biorouter serve --port 9999 --public-url https://proxy.example.org/prefix/

    --public-url does three things:

    • prints the address to open;
    • lets the live views connect even when a proxy chain rewrites Host or X-Forwarded-Proto;
    • scopes the access cookie to the prefix.

    The proxy must strip the prefix, pass WebSocket upgrades through, and leave the browser's Sec-Fetch-* headers in place. A plain http public URL is accepted only for a loopback address.

    One caution on --no-token

    On a hub where users' servers share one machine (LocalProcessSpawner, The Littlest JupyterHub), every other account can reach the loopback port. Keep the token there, passed in with BIOROUTER_BROWSER_TOKEN. With one container per server (KubeSpawner, DockerSpawner), --no-token behind the hub's login is reasonable. The docs have jupyter-server-proxy examples for both: docs/deployment/browser-access.md, section "Behind a reverse proxy".

    I verified this in Chrome through a proxy that mimics the JupyterHub chain: prefix stripped, Host preserved, X-Forwarded-Proto: https,http. The interface, its live views and a built app all work under /user/<name>/proxy/<port>/.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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