fastapi-reverse-proxy 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastapi_reverse_proxy-0.1.0/src/fastapi_reverse_proxy.egg-info → fastapi_reverse_proxy-0.2.0}/PKG-INFO +24 -26
- {fastapi_reverse_proxy-0.1.0 → fastapi_reverse_proxy-0.2.0}/README.md +23 -25
- {fastapi_reverse_proxy-0.1.0 → fastapi_reverse_proxy-0.2.0}/pyproject.toml +1 -1
- {fastapi_reverse_proxy-0.1.0 → fastapi_reverse_proxy-0.2.0}/src/fastapi_reverse_proxy/load_balance.py +3 -3
- fastapi_reverse_proxy-0.2.0/src/fastapi_reverse_proxy/proxy_httpx.py +30 -0
- {fastapi_reverse_proxy-0.1.0 → fastapi_reverse_proxy-0.2.0}/src/fastapi_reverse_proxy/proxy_pass.py +15 -4
- {fastapi_reverse_proxy-0.1.0 → fastapi_reverse_proxy-0.2.0/src/fastapi_reverse_proxy.egg-info}/PKG-INFO +24 -26
- fastapi_reverse_proxy-0.1.0/src/fastapi_reverse_proxy/proxy_httpx.py +0 -15
- {fastapi_reverse_proxy-0.1.0 → fastapi_reverse_proxy-0.2.0}/LICENSE +0 -0
- {fastapi_reverse_proxy-0.1.0 → fastapi_reverse_proxy-0.2.0}/setup.cfg +0 -0
- {fastapi_reverse_proxy-0.1.0 → fastapi_reverse_proxy-0.2.0}/src/fastapi_reverse_proxy/__init__.py +0 -0
- {fastapi_reverse_proxy-0.1.0 → fastapi_reverse_proxy-0.2.0}/src/fastapi_reverse_proxy/health_check.py +0 -0
- {fastapi_reverse_proxy-0.1.0 → fastapi_reverse_proxy-0.2.0}/src/fastapi_reverse_proxy.egg-info/SOURCES.txt +0 -0
- {fastapi_reverse_proxy-0.1.0 → fastapi_reverse_proxy-0.2.0}/src/fastapi_reverse_proxy.egg-info/dependency_links.txt +0 -0
- {fastapi_reverse_proxy-0.1.0 → fastapi_reverse_proxy-0.2.0}/src/fastapi_reverse_proxy.egg-info/requires.txt +0 -0
- {fastapi_reverse_proxy-0.1.0 → fastapi_reverse_proxy-0.2.0}/src/fastapi_reverse_proxy.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-reverse-proxy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A robust, streaming-capable reverse proxy for FastAPI including WebSocket support.
|
|
5
5
|
Author-email: Tomás <tomas@suricatingss.xyz>
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -29,53 +29,51 @@ A robust, streaming-capable reverse proxy for FastAPI/Starlette with built-in **
|
|
|
29
29
|
|
|
30
30
|
## Features
|
|
31
31
|
|
|
32
|
-
- **
|
|
32
|
+
- **Async**: Async by default.
|
|
33
|
+
- **Httpx Pool**: Async HTTPX Pool for proxying.
|
|
34
|
+
- **Streaming Ready**: Handles SSE (Server-Sent Events) and large payloads (such as big files) while keeping RAM usage low.
|
|
33
35
|
- **WebSocket Support**: Seamless bidirectional tunneling with automated subprotocol negotiation.
|
|
34
36
|
- **Unified Load Balancing**: Standard Round-Robin or Smart routing using a single utility.
|
|
35
37
|
- **Latency-Based Routing**: Automatically routes traffic to the fastest healthy server (HEAD probe).
|
|
36
38
|
- **Advanced Overrides**: Granular control over headers, body, and HTTP methods.
|
|
37
|
-
- **Robust Cancellation**: Specialized handling for `asyncio.CancelledError` to prevent resource leaks.
|
|
38
39
|
- **Version Agnostic**: Automatically handles `websockets` library version differences (12.0+ vs Legacy).
|
|
39
40
|
|
|
40
|
-
## Quick Start
|
|
41
|
+
## Quick Start
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
Use the **lifespan** handler as shown for an easy launch.
|
|
44
|
+
|
|
45
|
+
The simplest way to use the proxy is to use **proxy_pass** and/or **proxy_pass_websocket** on the endpoints.
|
|
43
46
|
|
|
44
47
|
```python
|
|
45
48
|
from fastapi import FastAPI, Request, WebSocket
|
|
46
49
|
from contextlib import asynccontextmanager
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
from __init__ import (
|
|
50
|
-
HealthChecker, LoadBalancer,
|
|
51
|
-
create_httpx_client, close_httpx_client
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
# 1. Setup health monitoring and load balancing
|
|
55
|
-
checker = HealthChecker(["http://localhost:8080", "http://localhost:8081"])
|
|
56
|
-
lb = LoadBalancer(checker)
|
|
51
|
+
from fastapi_reverse_proxy import Proxy, proxy_pass, proxy_pass_websocket
|
|
57
52
|
|
|
58
53
|
@asynccontextmanager
|
|
59
54
|
async def lifespan(app: FastAPI):
|
|
60
|
-
|
|
61
|
-
await create_httpx_client(app)
|
|
62
|
-
async with checker: # Starts background health loop
|
|
55
|
+
async with Proxy(app):
|
|
63
56
|
yield
|
|
64
|
-
await close_httpx_client(app)
|
|
65
57
|
|
|
66
58
|
app = FastAPI(lifespan=lifespan)
|
|
67
59
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
60
|
+
# catch-all route. recommended for a reverse proxy
|
|
61
|
+
@app.api_route("/{path:path}", methods=["GET","POST","PUT","DELETE"]) # don't forget to add the methods.
|
|
62
|
+
async def index(req: Request):
|
|
63
|
+
"""
|
|
64
|
+
You always need to pass the "Request" object and to specify the host
|
|
65
|
+
If you don't add a path, it will be the same as the original (/login --> http://127.0.0.1/login)
|
|
66
|
+
"""
|
|
67
|
+
return await proxy_pass(req, "http://127.0.0.1:8080")
|
|
72
68
|
|
|
73
|
-
@app.websocket("/ws/{path:path}")
|
|
74
|
-
async def ws_tunnel(websocket: WebSocket, path: str):
|
|
75
|
-
# Automatic subprotocol negotiation + Tunneling
|
|
76
|
-
await lb.proxy_pass_websocket(websocket, path=f"/{path}")
|
|
77
69
|
```
|
|
78
70
|
|
|
71
|
+
## Advanced Examples:
|
|
72
|
+
|
|
73
|
+
Check [example.py](example.py) for full examples, including
|
|
74
|
+
- **Websocket Proxy**
|
|
75
|
+
- **Socket.IO Proxy**
|
|
76
|
+
|
|
79
77
|
## Advanced Proxying
|
|
80
78
|
|
|
81
79
|
The `proxy_pass` function and `LoadBalancer.proxy_pass` provide deep customization for upstream requests:
|
|
@@ -4,53 +4,51 @@ A robust, streaming-capable reverse proxy for FastAPI/Starlette with built-in **
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **
|
|
7
|
+
- **Async**: Async by default.
|
|
8
|
+
- **Httpx Pool**: Async HTTPX Pool for proxying.
|
|
9
|
+
- **Streaming Ready**: Handles SSE (Server-Sent Events) and large payloads (such as big files) while keeping RAM usage low.
|
|
8
10
|
- **WebSocket Support**: Seamless bidirectional tunneling with automated subprotocol negotiation.
|
|
9
11
|
- **Unified Load Balancing**: Standard Round-Robin or Smart routing using a single utility.
|
|
10
12
|
- **Latency-Based Routing**: Automatically routes traffic to the fastest healthy server (HEAD probe).
|
|
11
13
|
- **Advanced Overrides**: Granular control over headers, body, and HTTP methods.
|
|
12
|
-
- **Robust Cancellation**: Specialized handling for `asyncio.CancelledError` to prevent resource leaks.
|
|
13
14
|
- **Version Agnostic**: Automatically handles `websockets` library version differences (12.0+ vs Legacy).
|
|
14
15
|
|
|
15
|
-
## Quick Start
|
|
16
|
+
## Quick Start
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
Use the **lifespan** handler as shown for an easy launch.
|
|
19
|
+
|
|
20
|
+
The simplest way to use the proxy is to use **proxy_pass** and/or **proxy_pass_websocket** on the endpoints.
|
|
18
21
|
|
|
19
22
|
```python
|
|
20
23
|
from fastapi import FastAPI, Request, WebSocket
|
|
21
24
|
from contextlib import asynccontextmanager
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
from __init__ import (
|
|
25
|
-
HealthChecker, LoadBalancer,
|
|
26
|
-
create_httpx_client, close_httpx_client
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
# 1. Setup health monitoring and load balancing
|
|
30
|
-
checker = HealthChecker(["http://localhost:8080", "http://localhost:8081"])
|
|
31
|
-
lb = LoadBalancer(checker)
|
|
26
|
+
from fastapi_reverse_proxy import Proxy, proxy_pass, proxy_pass_websocket
|
|
32
27
|
|
|
33
28
|
@asynccontextmanager
|
|
34
29
|
async def lifespan(app: FastAPI):
|
|
35
|
-
|
|
36
|
-
await create_httpx_client(app)
|
|
37
|
-
async with checker: # Starts background health loop
|
|
30
|
+
async with Proxy(app):
|
|
38
31
|
yield
|
|
39
|
-
await close_httpx_client(app)
|
|
40
32
|
|
|
41
33
|
app = FastAPI(lifespan=lifespan)
|
|
42
34
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
35
|
+
# catch-all route. recommended for a reverse proxy
|
|
36
|
+
@app.api_route("/{path:path}", methods=["GET","POST","PUT","DELETE"]) # don't forget to add the methods.
|
|
37
|
+
async def index(req: Request):
|
|
38
|
+
"""
|
|
39
|
+
You always need to pass the "Request" object and to specify the host
|
|
40
|
+
If you don't add a path, it will be the same as the original (/login --> http://127.0.0.1/login)
|
|
41
|
+
"""
|
|
42
|
+
return await proxy_pass(req, "http://127.0.0.1:8080")
|
|
47
43
|
|
|
48
|
-
@app.websocket("/ws/{path:path}")
|
|
49
|
-
async def ws_tunnel(websocket: WebSocket, path: str):
|
|
50
|
-
# Automatic subprotocol negotiation + Tunneling
|
|
51
|
-
await lb.proxy_pass_websocket(websocket, path=f"/{path}")
|
|
52
44
|
```
|
|
53
45
|
|
|
46
|
+
## Advanced Examples:
|
|
47
|
+
|
|
48
|
+
Check [example.py](example.py) for full examples, including
|
|
49
|
+
- **Websocket Proxy**
|
|
50
|
+
- **Socket.IO Proxy**
|
|
51
|
+
|
|
54
52
|
## Advanced Proxying
|
|
55
53
|
|
|
56
54
|
The `proxy_pass` function and `LoadBalancer.proxy_pass` provide deep customization for upstream requests:
|
|
@@ -158,12 +158,12 @@ class LoadBalancer:
|
|
|
158
158
|
|
|
159
159
|
u = urlparse(target)
|
|
160
160
|
origin = f"{u.scheme}://{u.netloc}"
|
|
161
|
-
|
|
162
|
-
dest_url = f"{origin.rstrip('/')}/{path.lstrip('/') if path is not None else websocket.url.path}"
|
|
161
|
+
|
|
163
162
|
|
|
164
163
|
return await _proxy_pass_ws(
|
|
165
164
|
websocket,
|
|
166
|
-
|
|
165
|
+
host=origin,
|
|
166
|
+
path=path,
|
|
167
167
|
subprotocols=subprotocols,
|
|
168
168
|
forward_query=forward_query,
|
|
169
169
|
additional_headers=additional_headers,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from fastapi import FastAPI, Request
|
|
3
|
+
|
|
4
|
+
class Proxy:
|
|
5
|
+
def __init__(self, app: FastAPI):
|
|
6
|
+
"""Initializes the HTTP client and stores it in the app state."""
|
|
7
|
+
self.__app = app # store the app as internal variable
|
|
8
|
+
self.__app.state.http_proxy_client = httpx.AsyncClient()
|
|
9
|
+
|
|
10
|
+
async def close(self):
|
|
11
|
+
"""Closes the HTTP client stored in the app state."""
|
|
12
|
+
if hasattr(self.__app.state, "http_proxy_client"):
|
|
13
|
+
await self.__app.state.http_proxy_client.aclose() # close it
|
|
14
|
+
|
|
15
|
+
async def __aenter__(self): return self # __init__ on async with
|
|
16
|
+
|
|
17
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb): await self.close() # self-close on async with
|
|
18
|
+
|
|
19
|
+
async def create_httpx_client(app: FastAPI):
|
|
20
|
+
"""Initializes the HTTP client and stores it in the app state."""
|
|
21
|
+
app.state.http_proxy_client = httpx.AsyncClient()
|
|
22
|
+
|
|
23
|
+
async def close_httpx_client(app: FastAPI):
|
|
24
|
+
"""Closes the HTTP client stored in the app state."""
|
|
25
|
+
if hasattr(app.state, "http_proxy_client"):
|
|
26
|
+
await app.state.http_proxy_client.aclose() # close it
|
|
27
|
+
|
|
28
|
+
async def get_httpx_client(req: Request) -> httpx.AsyncClient:
|
|
29
|
+
"""Retrieves the HTTP client from the app state."""
|
|
30
|
+
return req.app.state.http_proxy_client
|
{fastapi_reverse_proxy-0.1.0 → fastapi_reverse_proxy-0.2.0}/src/fastapi_reverse_proxy/proxy_pass.py
RENAMED
|
@@ -9,6 +9,7 @@ import logging
|
|
|
9
9
|
import inspect
|
|
10
10
|
from typing import Optional
|
|
11
11
|
from .proxy_httpx import get_httpx_client
|
|
12
|
+
from urllib.parse import urlparse
|
|
12
13
|
|
|
13
14
|
logger = logging.getLogger("fastapi_reverse_proxy")
|
|
14
15
|
|
|
@@ -19,6 +20,11 @@ EXCLUDED_HEADERS = {
|
|
|
19
20
|
"proxy-authorization", "te", "trailers", "transfer-encoding", "upgrade"
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
def url_normalize_ws(url:str):
|
|
24
|
+
u = urlparse(url)
|
|
25
|
+
return url_normalize(url.replace(u.scheme, "http", 1)).replace("http", u.scheme, 1)
|
|
26
|
+
|
|
27
|
+
|
|
22
28
|
async def proxy_pass(
|
|
23
29
|
request: Request,
|
|
24
30
|
host: str,
|
|
@@ -33,7 +39,7 @@ async def proxy_pass(
|
|
|
33
39
|
"""
|
|
34
40
|
Forwards incoming HTTP requests to the target service using streaming.
|
|
35
41
|
- host: The host itself (without ending slash)
|
|
36
|
-
- path: The path
|
|
42
|
+
- path: The path w/ beggining slash (by default copies requests's path)
|
|
37
43
|
- forward_query: If True, automatically appends the request's query string.
|
|
38
44
|
- additional_headers: Headers to add to the upstream request.
|
|
39
45
|
- override_headers: Use these headers instead of original request headers.
|
|
@@ -153,7 +159,8 @@ async def proxy_pass(
|
|
|
153
159
|
|
|
154
160
|
async def proxy_pass_websocket(
|
|
155
161
|
websocket: WebSocket,
|
|
156
|
-
|
|
162
|
+
host: str,
|
|
163
|
+
path: Optional[str] = None,
|
|
157
164
|
subprotocols: Optional[list[str]] = None,
|
|
158
165
|
forward_query: bool = True,
|
|
159
166
|
additional_headers: Optional[dict] = None,
|
|
@@ -161,10 +168,14 @@ async def proxy_pass_websocket(
|
|
|
161
168
|
):
|
|
162
169
|
"""
|
|
163
170
|
Forwards incoming WebSocket connections to the target service.
|
|
164
|
-
-
|
|
171
|
+
- host: The host itself (without ending slash)
|
|
172
|
+
- path: The path with beggining slash (by default copies requests's path)
|
|
165
173
|
- forward_query: If True, automatically appends the request's query string.
|
|
166
174
|
"""
|
|
167
|
-
|
|
175
|
+
|
|
176
|
+
if path is None: path = websocket.url.path
|
|
177
|
+
url = url_normalize_ws(host + path)
|
|
178
|
+
|
|
168
179
|
if forward_query and websocket.url.query:
|
|
169
180
|
url = f"{url}?{websocket.url.query}" if "?" not in url else f"{url}&{websocket.url.query}"
|
|
170
181
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-reverse-proxy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A robust, streaming-capable reverse proxy for FastAPI including WebSocket support.
|
|
5
5
|
Author-email: Tomás <tomas@suricatingss.xyz>
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -29,53 +29,51 @@ A robust, streaming-capable reverse proxy for FastAPI/Starlette with built-in **
|
|
|
29
29
|
|
|
30
30
|
## Features
|
|
31
31
|
|
|
32
|
-
- **
|
|
32
|
+
- **Async**: Async by default.
|
|
33
|
+
- **Httpx Pool**: Async HTTPX Pool for proxying.
|
|
34
|
+
- **Streaming Ready**: Handles SSE (Server-Sent Events) and large payloads (such as big files) while keeping RAM usage low.
|
|
33
35
|
- **WebSocket Support**: Seamless bidirectional tunneling with automated subprotocol negotiation.
|
|
34
36
|
- **Unified Load Balancing**: Standard Round-Robin or Smart routing using a single utility.
|
|
35
37
|
- **Latency-Based Routing**: Automatically routes traffic to the fastest healthy server (HEAD probe).
|
|
36
38
|
- **Advanced Overrides**: Granular control over headers, body, and HTTP methods.
|
|
37
|
-
- **Robust Cancellation**: Specialized handling for `asyncio.CancelledError` to prevent resource leaks.
|
|
38
39
|
- **Version Agnostic**: Automatically handles `websockets` library version differences (12.0+ vs Legacy).
|
|
39
40
|
|
|
40
|
-
## Quick Start
|
|
41
|
+
## Quick Start
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
Use the **lifespan** handler as shown for an easy launch.
|
|
44
|
+
|
|
45
|
+
The simplest way to use the proxy is to use **proxy_pass** and/or **proxy_pass_websocket** on the endpoints.
|
|
43
46
|
|
|
44
47
|
```python
|
|
45
48
|
from fastapi import FastAPI, Request, WebSocket
|
|
46
49
|
from contextlib import asynccontextmanager
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
from __init__ import (
|
|
50
|
-
HealthChecker, LoadBalancer,
|
|
51
|
-
create_httpx_client, close_httpx_client
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
# 1. Setup health monitoring and load balancing
|
|
55
|
-
checker = HealthChecker(["http://localhost:8080", "http://localhost:8081"])
|
|
56
|
-
lb = LoadBalancer(checker)
|
|
51
|
+
from fastapi_reverse_proxy import Proxy, proxy_pass, proxy_pass_websocket
|
|
57
52
|
|
|
58
53
|
@asynccontextmanager
|
|
59
54
|
async def lifespan(app: FastAPI):
|
|
60
|
-
|
|
61
|
-
await create_httpx_client(app)
|
|
62
|
-
async with checker: # Starts background health loop
|
|
55
|
+
async with Proxy(app):
|
|
63
56
|
yield
|
|
64
|
-
await close_httpx_client(app)
|
|
65
57
|
|
|
66
58
|
app = FastAPI(lifespan=lifespan)
|
|
67
59
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
60
|
+
# catch-all route. recommended for a reverse proxy
|
|
61
|
+
@app.api_route("/{path:path}", methods=["GET","POST","PUT","DELETE"]) # don't forget to add the methods.
|
|
62
|
+
async def index(req: Request):
|
|
63
|
+
"""
|
|
64
|
+
You always need to pass the "Request" object and to specify the host
|
|
65
|
+
If you don't add a path, it will be the same as the original (/login --> http://127.0.0.1/login)
|
|
66
|
+
"""
|
|
67
|
+
return await proxy_pass(req, "http://127.0.0.1:8080")
|
|
72
68
|
|
|
73
|
-
@app.websocket("/ws/{path:path}")
|
|
74
|
-
async def ws_tunnel(websocket: WebSocket, path: str):
|
|
75
|
-
# Automatic subprotocol negotiation + Tunneling
|
|
76
|
-
await lb.proxy_pass_websocket(websocket, path=f"/{path}")
|
|
77
69
|
```
|
|
78
70
|
|
|
71
|
+
## Advanced Examples:
|
|
72
|
+
|
|
73
|
+
Check [example.py](example.py) for full examples, including
|
|
74
|
+
- **Websocket Proxy**
|
|
75
|
+
- **Socket.IO Proxy**
|
|
76
|
+
|
|
79
77
|
## Advanced Proxying
|
|
80
78
|
|
|
81
79
|
The `proxy_pass` function and `LoadBalancer.proxy_pass` provide deep customization for upstream requests:
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import httpx
|
|
2
|
-
from fastapi import FastAPI, Request
|
|
3
|
-
|
|
4
|
-
async def create_httpx_client(app: FastAPI):
|
|
5
|
-
"""Initializes the HTTP client and stores it in the app state."""
|
|
6
|
-
app.state.http_proxy_client = httpx.AsyncClient()
|
|
7
|
-
|
|
8
|
-
async def close_httpx_client(app: FastAPI):
|
|
9
|
-
"""Closes the HTTP client stored in the app state."""
|
|
10
|
-
if hasattr(app.state, "http_proxy_client"):
|
|
11
|
-
await app.state.http_proxy_client.aclose() # close it
|
|
12
|
-
|
|
13
|
-
async def get_httpx_client(req: Request) -> httpx.AsyncClient:
|
|
14
|
-
"""Retrieves the HTTP client from the app state."""
|
|
15
|
-
return req.app.state.http_proxy_client
|
|
File without changes
|
|
File without changes
|
{fastapi_reverse_proxy-0.1.0 → fastapi_reverse_proxy-0.2.0}/src/fastapi_reverse_proxy/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|