ws-sse-proxy
A drop-in reverse proxy that transparently translates WebSocket connections to SSE + HTTP POST, for environments where WebSocket is blocked.
The Problem
Some deployment environments block or drop WebSocket connections: AWS ALBs that strip Connection: Upgrade headers, corporate proxies, reverse proxies with misconfigured WebSocket support, or platforms like AWS SageMaker Studio Lab where the gateway kills WebSocket on certain paths.
Your web application works fine on localhost but shows "connecting..." or blank content when deployed behind one of these proxies — because WebSocket never completes.
How It Works
Browser → Broken Proxy → ws-sse-proxy (port 8081) → WebSocket → Your App (port 8080)
(HTTP only) (localhost, works fine)
The proxy:
- Passes all HTTP through to your application unchanged
- Injects a tiny JavaScript shim into HTML responses that wraps
window.WebSocket - The shim tries real WebSocket first — if it works, there's zero overhead
- If WebSocket fails (code 1006 or connection stall), it falls back to SSE + POST
- The
/__wss/eventsendpoint opens a real WebSocket to your app on localhost and streams messages back as Server-Sent Events - The
/__wss/sendPOST endpoint forwards client messages over the local WebSocket
Your application doesn't need any changes. The proxy handles the translation.
Installation
pip install ws-sse-proxy
Usage
# Your app is running on port 8080
ws-sse-proxy --target-port 8080 --listen-port 8081
Then point your users (or proxy/gateway) at port 8081 instead of 8080.
All Options
ws-sse-proxy --target-port PORT --listen-port PORT [OPTIONS]
Required:
--target-port PORT Port your application is listening on
--listen-port PORT Port for the proxy to listen on
Optional:
--target-host HOST Target host (default: localhost)
--host HOST Bind address (default: 0.0.0.0)
--log-level LEVEL DEBUG, INFO, WARNING, or ERROR (default: INFO)
As a Python module
python -m ws_sse_proxy --target-port 8080 --listen-port 8081
Programmatic
from ws_sse_proxy.proxy import create_proxy
app = create_proxy(target_port=8080)
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8081)
Example: marimo on AWS SageMaker
marimo is a reactive Python notebook that requires WebSocket. On SageMaker Studio Lab, the gateway drops WebSocket on proxy paths. With ws-sse-proxy:
# Start marimo
marimo edit --host 0.0.0.0 --port 2718 --no-token --headless &
# Start the proxy in front of it
ws-sse-proxy --target-port 2718 --listen-port 2719
Access marimo at /proxy/2719/ — the proxy translates WebSocket to SSE automatically.
See aws-marimo-sagemaker for a complete setup.
How Detection Works
The injected JavaScript doesn't blindly replace WebSocket. It:
- Attempts a real WebSocket connection
- Sets a 3-second timeout for stalled connections
- If the WebSocket opens then immediately closes with code 1006 (abnormal closure — the signature of a proxy dropping the connection), falls back to SSE
- If WebSocket connects normally, uses it with zero overhead
This makes the proxy safe to use everywhere. In environments where WebSocket works, the real WebSocket is used.
Technical Details
The proxy uses /__wss/ as its namespace for shim endpoints, chosen to avoid collisions with application routes:
/__wss/events— SSE endpoint (server → client)/__wss/send— POST endpoint (client → server)/__wss/close— POST endpoint (cleanup)
All other paths are proxied to the target application. HTML responses get the JavaScript shim injected before the first <script> tag.
Dependencies: starlette, websockets, httpx, uvicorn — all pure Python, no compiled extensions required.
Development
# Install with test extras (using uv)
uv pip install -e '.[test]'
# Run the test suite
pytest
The suite covers HTTP pass-through, JavaScript shim injection (including gzip-encoded HTML), and the SSE↔WebSocket bridge end-to-end against a live proxy+target stack — text and binary round-trips, query-parameter forwarding, and connection cleanup.
Changelog
0.1.3
- Fix (#2): SSE
fallback now works when the proxy is served under a sub-path prefix
(jupyter-server-proxy, JupyterHub, SageMaker Studio). The injected shim
previously assumed a root mount and requested
/__wss/*at the wrong path, causing a 404 retry loop. It now derives the mount prefix from the page and WebSocket URLs and forwards the app-relative WebSocket path.
0.1.1
- Fix: query parameters forwarded to the target WebSocket are now
percent-encoded. Values containing spaces,
&, or=(e.g. auth tokens) previously corrupted the WebSocket handshake. - Add: test suite.
0.1.0
- Initial release.
License
MIT
Metadata
Release files for ws-sse-proxy 0.1.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ws_sse_proxy-0.1.3.tar.gz | 47.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ws_sse_proxy-0.1.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 59.7 kB
Release files / ws_sse_proxy-0.1.3.tar.gz
| Download URL | ws_sse_proxy-0.1.3.tar.gz |
|---|---|
| Size | 47.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
17f208542fb5be14479f97e2b39f6f49b0bc98e7cc49e9abd04931878142df36
|
|
BLAKE2b-256 checksum How to use checksums |
45114e9717d115f88f45553f6109c65d3d484f5df97795bc65e6b60d8a506927
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 9, 2026.
Transparency logRelease files / ws_sse_proxy-0.1.3-py3-none-any.whl
| Download URL | ws_sse_proxy-0.1.3-py3-none-any.whl |
|---|---|
| Size | 11.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f296ebcad9ea3d7d5df1b7565aa77ca2cfc5f237ce2f970dd13c366729bde8f2
|
|
BLAKE2b-256 checksum How to use checksums |
2f9c1220d62fc1f070014d13863316d59e3cbb505161cbe5087134f8eb0fe539
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 9, 2026.
Transparency log