Metadata-Version: 2.4
Name: ws-sse-proxy
Version: 0.1.0
Summary: A drop-in proxy that translates WebSocket connections to SSE + HTTP POST, for environments where WebSocket is blocked
Project-URL: Homepage, https://github.com/scttfrdmn/ws-sse-proxy
Project-URL: Issues, https://github.com/scttfrdmn/ws-sse-proxy/issues
Project-URL: Repository, https://github.com/scttfrdmn/ws-sse-proxy
Author-email: Scott Friedman <scttfrdmn@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: alb,jupyter,proxy,sagemaker,server-sent-events,sse,websocket
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: Proxy Servers
Classifier: Topic :: Internet :: WWW/HTTP
Requires-Python: >=3.9
Requires-Dist: httpx>=0.24.0
Requires-Dist: starlette>=0.27.0
Requires-Dist: uvicorn>=0.22.0
Requires-Dist: websockets>=11.0
Description-Content-Type: text/markdown

# 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:

1. **Passes all HTTP through** to your application unchanged
2. **Injects a tiny JavaScript shim** into HTML responses that wraps `window.WebSocket`
3. The shim **tries real WebSocket first** — if it works, there's zero overhead
4. If WebSocket fails (code 1006 or connection stall), it **falls back to SSE + POST**
5. The `/__wss/events` endpoint opens a real WebSocket to your app on localhost and streams messages back as Server-Sent Events
6. The `/__wss/send` POST endpoint forwards client messages over the local WebSocket

Your application doesn't need any changes. The proxy handles the translation.

## Installation

```bash
pip install ws-sse-proxy
```

## Usage

```bash
# 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

```bash
python -m ws_sse_proxy --target-port 8080 --listen-port 8081
```

### Programmatic

```python
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](https://marimo.io) is a reactive Python notebook that requires WebSocket. On SageMaker Studio Lab, the gateway drops WebSocket on proxy paths. With ws-sse-proxy:

```bash
# 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](https://github.com/scttfrdmn/aws-marimo-sagemaker) for a complete setup.

## How Detection Works

The injected JavaScript doesn't blindly replace WebSocket. It:

1. Attempts a real WebSocket connection
2. Sets a 3-second timeout for stalled connections
3. If the WebSocket opens then immediately closes with code 1006 (abnormal closure — the signature of a proxy dropping the connection), falls back to SSE
4. 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.

## License

MIT
