Make kernel-local HTTP/WS servers reachable from the notebook browser with zero user config.
Project description
Make kernel-local HTTP and WebSocket servers reachable from the notebook browser. Zero user config.
If your library runs a web server inside a Jupyter kernel (a tile server, a trame app, Bokeh, Dash, a custom debug UI, anything bound to 127.0.0.1:<port>), your users hit the same wall every time. Works locally. Breaks on JupyterHub, MyBinder, VS Code Remote, Colab, Shiny. The usual fix is a README paragraph telling users to install jupyter-server-proxy, export LIBRARY_CLIENT_PREFIX='proxy/{port}', and prepend $JUPYTERHUB_SERVICE_PREFIX on Hub. Most users skip it, get a broken notebook, and file an issue.
jupyter-loopback replaces that paragraph. Libraries register once. End users configure nothing.
Who this is for
Library authors who spin up an HTTP or WebSocket server inside a Jupyter kernel and need the browser to reach it without asking every user to configure a proxy. If you've ever written "make sure to install jupyter-server-proxy and set FOO_CLIENT_PREFIX='proxy/{port}'" in your README, this is for you.
Install
pip install jupyter-loopback # HTTP/WS proxy
pip install jupyter-loopback[comm] # + anywidget comm bridge fallback
The 30-second demo
docker build -t jupyter-loopback-demo .
docker run --rm -it -p 8888:8888 jupyter-loopback-demo
Open the printed token URL, run example.ipynb. You'll see:
- A JSON response fetched through the proxy.
- A red square rendered inline (binary-body correctness check).
- A live WebSocket echo box you can type into.
All of it flowing through <base_url>/loopback-demo-proxy/<port>/… with no config.
For library authors
Suppose your library is mylib and it spins up a server at 127.0.0.1:<port> inside the kernel. Three files wire it up.
1. Server-side: register the proxy
# mylib/_jupyter/__init__.py
from jupyter_loopback import setup_proxy_handler
def _jupyter_server_extension_points():
return [{"module": "mylib._jupyter"}]
def _load_jupyter_server_extension(server_app):
setup_proxy_handler(server_app.web_app, namespace="mylib")
2. Auto-enable the extension
Ship jupyter-config/jupyter_server_config.d/mylib.json:
{
"ServerApp": {
"jpserver_extensions": {
"mylib._jupyter": true
}
}
}
And wire it into pyproject.toml:
[tool.setuptools.data-files]
"etc/jupyter/jupyter_server_config.d" = [
"jupyter-config/jupyter_server_config.d/mylib.json",
]
3. Kernel-side: build browser-reachable URLs
from jupyter_loopback import autodetect_prefix
def browser_url(port: int, path: str) -> str:
prefix = autodetect_prefix("mylib") # None outside Jupyter
if prefix is None:
return f"http://127.0.0.1:{port}/{path.lstrip('/')}"
return f"{prefix.format(port=port)}/{path.lstrip('/')}"
That's it. In a local Python REPL, autodetect_prefix returns None and you hit 127.0.0.1 directly. In JupyterLab, Hub, Binder, or any jupyter-server environment, it returns mylib-proxy/{port} (with any per-user Hub prefix already attached) and the browser loads through the proxy.
For users on VS Code Remote, Colab, Shiny, Solara, marimo
These frontends don't run a jupyter-server, so the HTTP proxy above isn't available. They do have kernel comms (the WebSocket the notebook widgets use). jupyter-loopback ships an anywidget that tunnels request/response pairs over that comm channel.
Users enable it once at the top of a notebook:
import jupyter_loopback
jupyter_loopback.enable_comm_bridge()
Library authors register handlers:
from jupyter_loopback import on_request
@on_request("mylib", "get_tile")
def _(data, buffers):
z, x, y = data["z"], data["x"], data["y"]
return {"ok": True}, [make_tile(z, x, y)] # (json, buffers)
Frontend code calls through window.__jupyter_loopback__:
const { status, data, buffers } = await window.__jupyter_loopback__.request(
"mylib",
"get_tile",
{ z: 8, x: 71, y: 110 },
);
if (status === "ok") {
const blob = new Blob([buffers[0]], { type: "image/png" });
imgElement.src = URL.createObjectURL(blob);
}
The bridge carries JSON plus binary buffers. Use it for anything request/response shaped. Streaming and server-push are out of scope; use the WS proxy for that.
What works where
| Environment | Path | User does |
|---|---|---|
| Local notebook | direct loopback | nothing |
| JupyterLab / Notebook 7+ | HTTP/WS proxy | nothing |
| JupyterHub / MyBinder | HTTP/WS proxy | nothing |
| VS Code Remote | comm bridge | enable_comm_bridge() |
| Google Colab | comm bridge | enable_comm_bridge() |
| Shiny for Python / Solara | comm bridge | enable_comm_bridge() |
| marimo | comm bridge | enable_comm_bridge() |
Relationship to jupyter-server-proxy
jupyter-server-proxy proxies arbitrary subprocesses. It handles HTTP/WS wire formatting, subprocess lifecycle, URL rewriting, auth.
jupyter-loopback does less, on purpose:
- Proxies loopback only. No cross-host surface.
- No subprocess management. You bring your own server on any port.
- Autodetects the URL prefix from Jupyter's own env vars.
- Ships a comm-based fallback for frontends without a jupyter-server.
The two can coexist. Set LIBRARY_CLIENT_PREFIX explicitly and jupyter-loopback's autodetect steps out of the way.
Status
Extracted from localtileserver after the same pattern solved its long tail of remote-Jupyter issues. Generalized so other libraries can adopt it without reinventing the wheel.
See ARCHITECTURE.md for the design walkthrough. MIT licensed.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file jupyter_loopback-0.3.3.tar.gz.
File metadata
- Download URL: jupyter_loopback-0.3.3.tar.gz
- Upload date:
- Size: 206.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3de29971d7148e8c369e41beedfbb03b301f518f03c06eb8cb26e7f1fcbd1b04
|
|
| MD5 |
980b098d1f417df858d1f2c57f568705
|
|
| BLAKE2b-256 |
9525576d4197b6cc062f0611753fb0165732624e79eef3ee4490df5c2abcd966
|
Provenance
The following attestation bundles were made for jupyter_loopback-0.3.3.tar.gz:
Publisher:
release.yml on banesullivan/jupyter-loopback
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
jupyter_loopback-0.3.3.tar.gz -
Subject digest:
3de29971d7148e8c369e41beedfbb03b301f518f03c06eb8cb26e7f1fcbd1b04 - Sigstore transparency entry: 1366224536
- Sigstore integration time:
-
Permalink:
banesullivan/jupyter-loopback@e3a82639be13a47ecce69532f39bb1db3cc082ef -
Branch / Tag:
refs/tags/v0.3.3 - Owner: https://github.com/banesullivan
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e3a82639be13a47ecce69532f39bb1db3cc082ef -
Trigger Event:
push
-
Statement type:
File details
Details for the file jupyter_loopback-0.3.3-py3-none-any.whl.
File metadata
- Download URL: jupyter_loopback-0.3.3-py3-none-any.whl
- Upload date:
- Size: 37.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7bab958ebb189530f0469d721e4c4743dc8c14022c31fea9b6cb8d850c9cea9a
|
|
| MD5 |
6cc5f1ea1d6775faf477cba16b6ef7be
|
|
| BLAKE2b-256 |
437e7b9a25a74810f979fc2c667a1e3efa2d4a1b44e743f5df166404dfb8b9fd
|
Provenance
The following attestation bundles were made for jupyter_loopback-0.3.3-py3-none-any.whl:
Publisher:
release.yml on banesullivan/jupyter-loopback
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
jupyter_loopback-0.3.3-py3-none-any.whl -
Subject digest:
7bab958ebb189530f0469d721e4c4743dc8c14022c31fea9b6cb8d850c9cea9a - Sigstore transparency entry: 1366224579
- Sigstore integration time:
-
Permalink:
banesullivan/jupyter-loopback@e3a82639be13a47ecce69532f39bb1db3cc082ef -
Branch / Tag:
refs/tags/v0.3.3 - Owner: https://github.com/banesullivan
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e3a82639be13a47ecce69532f39bb1db3cc082ef -
Trigger Event:
push
-
Statement type: