fastapi-reverse-proxy 0.3.1__tar.gz → 0.4.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.3.1 → fastapi_reverse_proxy-0.4.0}/PKG-INFO +23 -2
- fastapi_reverse_proxy-0.3.1/src/fastapi_reverse_proxy.egg-info/PKG-INFO → fastapi_reverse_proxy-0.4.0/README.md +20 -26
- {fastapi_reverse_proxy-0.3.1 → fastapi_reverse_proxy-0.4.0}/pyproject.toml +5 -1
- {fastapi_reverse_proxy-0.3.1 → fastapi_reverse_proxy-0.4.0}/src/fastapi_reverse_proxy/proxy_pass.py +45 -49
- fastapi_reverse_proxy-0.3.1/README.md → fastapi_reverse_proxy-0.4.0/src/fastapi_reverse_proxy.egg-info/PKG-INFO +47 -1
- {fastapi_reverse_proxy-0.3.1 → fastapi_reverse_proxy-0.4.0}/LICENSE +0 -0
- {fastapi_reverse_proxy-0.3.1 → fastapi_reverse_proxy-0.4.0}/setup.cfg +0 -0
- {fastapi_reverse_proxy-0.3.1 → fastapi_reverse_proxy-0.4.0}/src/fastapi_reverse_proxy/__init__.py +0 -0
- {fastapi_reverse_proxy-0.3.1 → fastapi_reverse_proxy-0.4.0}/src/fastapi_reverse_proxy/health_check.py +0 -0
- {fastapi_reverse_proxy-0.3.1 → fastapi_reverse_proxy-0.4.0}/src/fastapi_reverse_proxy/load_balance.py +0 -0
- {fastapi_reverse_proxy-0.3.1 → fastapi_reverse_proxy-0.4.0}/src/fastapi_reverse_proxy/proxy_httpx.py +0 -0
- {fastapi_reverse_proxy-0.3.1 → fastapi_reverse_proxy-0.4.0}/src/fastapi_reverse_proxy.egg-info/SOURCES.txt +0 -0
- {fastapi_reverse_proxy-0.3.1 → fastapi_reverse_proxy-0.4.0}/src/fastapi_reverse_proxy.egg-info/dependency_links.txt +0 -0
- {fastapi_reverse_proxy-0.3.1 → fastapi_reverse_proxy-0.4.0}/src/fastapi_reverse_proxy.egg-info/requires.txt +0 -0
- {fastapi_reverse_proxy-0.3.1 → fastapi_reverse_proxy-0.4.0}/src/fastapi_reverse_proxy.egg-info/top_level.txt +0 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-reverse-proxy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/tfsantos05/fastapi-reverse-proxy
|
|
8
|
+
Project-URL: Issues, https://github.com/tfsantos05/fastapi-reverse-proxy/issues
|
|
7
9
|
Classifier: Programming Language :: Python :: 3
|
|
8
10
|
Classifier: Operating System :: OS Independent
|
|
9
11
|
Classifier: Framework :: FastAPI
|
|
@@ -93,7 +95,7 @@ The `proxy_pass` function and `LoadBalancer.proxy_pass` provide deep customizati
|
|
|
93
95
|
| :--- | :--- | :--- |
|
|
94
96
|
| `timeout` | `float` | Total request timeout in seconds (Default: `60.0`). |
|
|
95
97
|
| `method` | `str` | Force a specific HTTP method (e.g., `"POST"`). |
|
|
96
|
-
| `override_body` | `bytes` |
|
|
98
|
+
| `override_body` | `bytes \| str \| dict \| list` | Use this instead of streaming the request body. `str` is UTF-8 encoded; `dict`/`list` are JSON-serialized automatically (automatically sets `Content-Type: application/json` if not already set). |
|
|
97
99
|
| `additional_headers` | `dict` | Append custom headers to the proxied request. |
|
|
98
100
|
| `override_headers` | `dict` | Use these headers *instead* of original request headers. |
|
|
99
101
|
| `forward_query` | `bool` | Whether to append the incoming query string (Default: `True`). |
|
|
@@ -129,6 +131,8 @@ The library implements "deferred negotiation" for WebSockets:
|
|
|
129
131
|
3. Once the upstream accepts a protocol, the proxy calls `websocket.accept(subprotocol=...)` back to the client.
|
|
130
132
|
4. This ensures the entire tunnel (Client <-> Proxy <-> Upstream) uses the same negotiated protocol.
|
|
131
133
|
5. **Handshake Timeout**: Supports a customizable `timeout` parameter (default `10.0s`) to prevent hangs if the backend is unresponsive.
|
|
134
|
+
6. **Error Handling**: Raises `fastapi.WebSocketException` when the upstream connection fails or is rejected with proper WS codes such as 1008 or 1011
|
|
135
|
+
7. **Debug**: Disruptions from either side are logged on debug level.
|
|
132
136
|
|
|
133
137
|
## Robustness & Safety
|
|
134
138
|
|
|
@@ -139,6 +143,14 @@ The library implements "deferred negotiation" for WebSockets:
|
|
|
139
143
|
`Proxy-Authorization`) are stripped from both outbound requests and responses,
|
|
140
144
|
per spec. WebSocket handshake headers (`Sec-WebSocket-Key`, `Upgrade`, etc.)
|
|
141
145
|
from the client are never forwarded to the target, avoiding handshake collisions.
|
|
146
|
+
- **Content-Length Safety**: Always stripped from the outbound request and
|
|
147
|
+
recalculated by `httpx` (or sent chunked when streaming), preventing
|
|
148
|
+
`LocalProtocolError` when `override_body` differs in size from the original
|
|
149
|
+
request.
|
|
150
|
+
- **Content-Encoding Passthrough**: Compressed upstream responses (`gzip`, `br`,
|
|
151
|
+
etc.) are streamed back to the client raw via `aiter_raw()`, with
|
|
152
|
+
`Content-Encoding` preserved — the client decompresses it itself, avoiding
|
|
153
|
+
corrupted/garbled response bodies.
|
|
142
154
|
|
|
143
155
|
|
|
144
156
|
|
|
@@ -181,3 +193,12 @@ location /ws/ {
|
|
|
181
193
|
|
|
182
194
|
Without this configuration, `X-Forwarded-*` headers will be empty or missing by
|
|
183
195
|
the time they reach your application.
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
### Streaming & Response Buffering
|
|
199
|
+
|
|
200
|
+
`X-Accel-Buffering: no` is set automatically on every proxied response to
|
|
201
|
+
disable Nginx's response buffering for streamed content (SSE, chunked
|
|
202
|
+
transfers, large file downloads). If deploying behind Apache instead, disable
|
|
203
|
+
buffering via your Apache config (`mod_proxy` directives) — there's no
|
|
204
|
+
equivalent response header Apache recognizes.
|
|
@@ -1,28 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: fastapi-reverse-proxy
|
|
3
|
-
Version: 0.3.1
|
|
4
|
-
Summary: A robust, streaming-capable reverse proxy for FastAPI including WebSocket support.
|
|
5
|
-
Author-email: Tomás <tomas@suricatingss.xyz>
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
Classifier: Programming Language :: Python :: 3
|
|
8
|
-
Classifier: Operating System :: OS Independent
|
|
9
|
-
Classifier: Framework :: FastAPI
|
|
10
|
-
Classifier: Topic :: Internet :: Proxy Servers
|
|
11
|
-
Requires-Python: >=3.8
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
13
|
-
License-File: LICENSE
|
|
14
|
-
Requires-Dist: fastapi>=0.129.0
|
|
15
|
-
Requires-Dist: httpx>=0.28.1
|
|
16
|
-
Requires-Dist: starlette>=0.52.1
|
|
17
|
-
Requires-Dist: uvicorn>=0.41.0
|
|
18
|
-
Requires-Dist: anyio>=4.12.1
|
|
19
|
-
Requires-Dist: certifi>=2026.1.4
|
|
20
|
-
Requires-Dist: pydantic>=2.12.5
|
|
21
|
-
Requires-Dist: url-normalize>=2.2.1
|
|
22
|
-
Requires-Dist: typing_extensions>=4.15.0
|
|
23
|
-
Requires-Dist: websockets>=16.0
|
|
24
|
-
Dynamic: license-file
|
|
25
|
-
|
|
26
1
|
# FastAPI Reverse Proxy
|
|
27
2
|
|
|
28
3
|
A robust, streaming-capable reverse proxy for FastAPI/Starlette with built-in **Latency-Based Load Balancing** and **Active Health Monitoring**.
|
|
@@ -93,7 +68,7 @@ The `proxy_pass` function and `LoadBalancer.proxy_pass` provide deep customizati
|
|
|
93
68
|
| :--- | :--- | :--- |
|
|
94
69
|
| `timeout` | `float` | Total request timeout in seconds (Default: `60.0`). |
|
|
95
70
|
| `method` | `str` | Force a specific HTTP method (e.g., `"POST"`). |
|
|
96
|
-
| `override_body` | `bytes` |
|
|
71
|
+
| `override_body` | `bytes \| str \| dict \| list` | Use this instead of streaming the request body. `str` is UTF-8 encoded; `dict`/`list` are JSON-serialized automatically (automatically sets `Content-Type: application/json` if not already set). |
|
|
97
72
|
| `additional_headers` | `dict` | Append custom headers to the proxied request. |
|
|
98
73
|
| `override_headers` | `dict` | Use these headers *instead* of original request headers. |
|
|
99
74
|
| `forward_query` | `bool` | Whether to append the incoming query string (Default: `True`). |
|
|
@@ -129,6 +104,8 @@ The library implements "deferred negotiation" for WebSockets:
|
|
|
129
104
|
3. Once the upstream accepts a protocol, the proxy calls `websocket.accept(subprotocol=...)` back to the client.
|
|
130
105
|
4. This ensures the entire tunnel (Client <-> Proxy <-> Upstream) uses the same negotiated protocol.
|
|
131
106
|
5. **Handshake Timeout**: Supports a customizable `timeout` parameter (default `10.0s`) to prevent hangs if the backend is unresponsive.
|
|
107
|
+
6. **Error Handling**: Raises `fastapi.WebSocketException` when the upstream connection fails or is rejected with proper WS codes such as 1008 or 1011
|
|
108
|
+
7. **Debug**: Disruptions from either side are logged on debug level.
|
|
132
109
|
|
|
133
110
|
## Robustness & Safety
|
|
134
111
|
|
|
@@ -139,6 +116,14 @@ The library implements "deferred negotiation" for WebSockets:
|
|
|
139
116
|
`Proxy-Authorization`) are stripped from both outbound requests and responses,
|
|
140
117
|
per spec. WebSocket handshake headers (`Sec-WebSocket-Key`, `Upgrade`, etc.)
|
|
141
118
|
from the client are never forwarded to the target, avoiding handshake collisions.
|
|
119
|
+
- **Content-Length Safety**: Always stripped from the outbound request and
|
|
120
|
+
recalculated by `httpx` (or sent chunked when streaming), preventing
|
|
121
|
+
`LocalProtocolError` when `override_body` differs in size from the original
|
|
122
|
+
request.
|
|
123
|
+
- **Content-Encoding Passthrough**: Compressed upstream responses (`gzip`, `br`,
|
|
124
|
+
etc.) are streamed back to the client raw via `aiter_raw()`, with
|
|
125
|
+
`Content-Encoding` preserved — the client decompresses it itself, avoiding
|
|
126
|
+
corrupted/garbled response bodies.
|
|
142
127
|
|
|
143
128
|
|
|
144
129
|
|
|
@@ -181,3 +166,12 @@ location /ws/ {
|
|
|
181
166
|
|
|
182
167
|
Without this configuration, `X-Forwarded-*` headers will be empty or missing by
|
|
183
168
|
the time they reach your application.
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
### Streaming & Response Buffering
|
|
172
|
+
|
|
173
|
+
`X-Accel-Buffering: no` is set automatically on every proxied response to
|
|
174
|
+
disable Nginx's response buffering for streamed content (SSE, chunked
|
|
175
|
+
transfers, large file downloads). If deploying behind Apache instead, disable
|
|
176
|
+
buffering via your Apache config (`mod_proxy` directives) — there's no
|
|
177
|
+
equivalent response header Apache recognizes.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fastapi-reverse-proxy"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Tomás", email="tomas@suricatingss.xyz" },
|
|
10
10
|
]
|
|
@@ -31,5 +31,9 @@ dependencies = [
|
|
|
31
31
|
"websockets>=16.0"
|
|
32
32
|
]
|
|
33
33
|
|
|
34
|
+
[project.urls]
|
|
35
|
+
Repository = "https://github.com/tfsantos05/fastapi-reverse-proxy"
|
|
36
|
+
Issues = "https://github.com/tfsantos05/fastapi-reverse-proxy/issues"
|
|
37
|
+
|
|
34
38
|
[tool.setuptools.packages.find]
|
|
35
39
|
where = ["src"]
|
{fastapi_reverse_proxy-0.3.1 → fastapi_reverse_proxy-0.4.0}/src/fastapi_reverse_proxy/proxy_pass.py
RENAMED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
from fastapi import Request, WebSocket,
|
|
1
|
+
from fastapi import Request, WebSocket, HTTPException, WebSocketException
|
|
2
|
+
from fastapi import status as http_codes
|
|
3
|
+
from fastapi.websockets import WebSocketState
|
|
2
4
|
from fastapi.responses import StreamingResponse
|
|
3
5
|
from starlette.background import BackgroundTask
|
|
4
6
|
from url_normalize import url_normalize
|
|
5
7
|
import httpx
|
|
6
8
|
import websockets
|
|
7
9
|
import asyncio
|
|
10
|
+
import json
|
|
8
11
|
import logging
|
|
9
12
|
import inspect
|
|
10
13
|
from typing import Optional
|
|
@@ -50,7 +53,7 @@ async def proxy_pass(
|
|
|
50
53
|
forward_query: bool = True,
|
|
51
54
|
additional_headers: Optional[dict] = None,
|
|
52
55
|
override_headers: Optional[dict] = None,
|
|
53
|
-
override_body: Optional[bytes] = None,
|
|
56
|
+
override_body: Optional[bytes | list | dict | str] = None,
|
|
54
57
|
method: Optional[str] = None,
|
|
55
58
|
override_host: Optional[str] = None
|
|
56
59
|
):
|
|
@@ -84,16 +87,6 @@ async def proxy_pass(
|
|
|
84
87
|
headers = dict(override_headers)
|
|
85
88
|
else:
|
|
86
89
|
headers = dict(request.headers)
|
|
87
|
-
# Identify the client's real IP and forward it
|
|
88
|
-
client_host = request.client.host if request.client else "unknown"
|
|
89
|
-
#headers["X-Real-IP"] = client_host
|
|
90
|
-
#if "X-Forwarded-For" in headers:
|
|
91
|
-
# headers["X-Forwarded-For"] = f"{headers['X-Forwarded-For']}, {client_host}"
|
|
92
|
-
#else:
|
|
93
|
-
# headers["X-Forwarded-For"] = client_host
|
|
94
|
-
|
|
95
|
-
#headers["X-Forwarded-Proto"] = request.url.scheme
|
|
96
|
-
#headers["X-Forwarded-Host"] = headers.get("host", request.url.netloc)
|
|
97
90
|
|
|
98
91
|
# Apply additional headers
|
|
99
92
|
if additional_headers:
|
|
@@ -109,6 +102,9 @@ async def proxy_pass(
|
|
|
109
102
|
# Let httpx handle connection management
|
|
110
103
|
headers.pop("connection", None)
|
|
111
104
|
|
|
105
|
+
# HTTPX should calculate this one automatically. No point in keeping it
|
|
106
|
+
headers.pop("content-length", None)
|
|
107
|
+
|
|
112
108
|
# Remove hop-to-hop headers
|
|
113
109
|
headers = {k: v for k, v in headers.items() if k.lower() not in EXCLUDED_HEADERS}
|
|
114
110
|
|
|
@@ -124,7 +120,16 @@ async def proxy_pass(
|
|
|
124
120
|
try:
|
|
125
121
|
# Prepare content
|
|
126
122
|
if override_body is not None:
|
|
127
|
-
|
|
123
|
+
if isinstance(override_body, (list, dict)):
|
|
124
|
+
# Stringify the JSON and convert to binary
|
|
125
|
+
content = json.dumps(override_body).encode()
|
|
126
|
+
# If no content type is set, set to JSON
|
|
127
|
+
headers.setdefault("content-type","application/json")
|
|
128
|
+
elif isinstance(override_body, str):
|
|
129
|
+
# Convert to binary if it's a string
|
|
130
|
+
content = override_body.encode()
|
|
131
|
+
else:
|
|
132
|
+
content = override_body
|
|
128
133
|
else:
|
|
129
134
|
# Stream the request body to the target (efficient for large uploads)
|
|
130
135
|
async def request_generator():
|
|
@@ -150,14 +155,12 @@ async def proxy_pass(
|
|
|
150
155
|
for k, v in rp_resp.headers.items():
|
|
151
156
|
if k.lower() in EXCLUDED_HEADERS:
|
|
152
157
|
continue
|
|
153
|
-
if k.lower() == "content-encoding":
|
|
154
|
-
continue
|
|
155
158
|
if k.lower() == "content-length":
|
|
156
159
|
continue
|
|
157
160
|
resp_headers[k] = v
|
|
158
|
-
|
|
161
|
+
|
|
162
|
+
# Prevents NGINX from buffering
|
|
159
163
|
resp_headers["X-Accel-Buffering"] = "no"
|
|
160
|
-
#resp_headers["Cache-Control"] = "no-cache"
|
|
161
164
|
|
|
162
165
|
async def cleanup():
|
|
163
166
|
await rp_resp.aclose()
|
|
@@ -165,7 +168,7 @@ async def proxy_pass(
|
|
|
165
168
|
await client.aclose()
|
|
166
169
|
|
|
167
170
|
return StreamingResponse(
|
|
168
|
-
rp_resp.
|
|
171
|
+
rp_resp.aiter_raw(),
|
|
169
172
|
status_code=rp_resp.status_code,
|
|
170
173
|
headers=resp_headers,
|
|
171
174
|
background=BackgroundTask(cleanup)
|
|
@@ -221,14 +224,8 @@ async def proxy_pass_websocket(
|
|
|
221
224
|
if override_headers is not None:
|
|
222
225
|
headers = dict(override_headers)
|
|
223
226
|
else:
|
|
224
|
-
client_host = websocket.client.host if websocket.client else "unknown"
|
|
227
|
+
#client_host = websocket.client.host if websocket.client else "unknown"
|
|
225
228
|
headers = dict(websocket.headers)
|
|
226
|
-
#headers = {
|
|
227
|
-
# "X-Real-IP": client_host,
|
|
228
|
-
# "X-Forwarded-For": client_host,
|
|
229
|
-
# "X-Forwarded-Proto": websocket.url.scheme,
|
|
230
|
-
# "X-Forwarded-Host": websocket.headers.get("host", websocket.url.netloc)
|
|
231
|
-
#}
|
|
232
229
|
|
|
233
230
|
if additional_headers:
|
|
234
231
|
headers.update(additional_headers)
|
|
@@ -256,33 +253,28 @@ async def proxy_pass_websocket(
|
|
|
256
253
|
await websocket.accept(subprotocol=target_ws.subprotocol)
|
|
257
254
|
await _handle_ws_bidirectional(websocket, target_ws)
|
|
258
255
|
|
|
256
|
+
# Graceful close
|
|
257
|
+
if websocket.client_state == WebSocketState.CONNECTED:
|
|
258
|
+
await websocket.close()
|
|
259
|
+
|
|
259
260
|
except websockets.exceptions.InvalidStatus as e:
|
|
260
261
|
status = e.response.status_code
|
|
261
262
|
logger.error(f"WebSocket handshake rejected by upstream: {status}")
|
|
262
|
-
|
|
263
|
-
raise HTTPException(status_code=status, detail=f"Upstream rejected WebSocket handshake: {status}")
|
|
264
|
-
except RuntimeError: # If already accepted, we can't raise HTTPException
|
|
265
|
-
pass
|
|
266
|
-
raise e
|
|
263
|
+
raise WebSocketException(code=http_codes.WS_1008_POLICY_VIOLATION, reason=f"WebSocket handshake rejected by upstream: {status}"[:123])
|
|
267
264
|
|
|
268
265
|
except BaseException as e:
|
|
269
266
|
if not isinstance(e, asyncio.CancelledError):
|
|
270
|
-
|
|
271
|
-
if not websocket.client.connected: # Roughly checking if handshake finished
|
|
267
|
+
if websocket.client_state != WebSocketState.CONNECTED: # Roughly checking if handshake finished
|
|
272
268
|
logger.error(f"WebSocket Connection Error: {e}")
|
|
273
|
-
|
|
274
|
-
try:
|
|
275
|
-
raise HTTPException(status_code=502, detail="Bad Gateway: WebSocket connection failed")
|
|
276
|
-
except RuntimeError: # If already accepted, we can't raise HTTPException
|
|
277
|
-
pass
|
|
269
|
+
raise WebSocketException(code=http_codes.WS_1011_INTERNAL_ERROR, reason=f"Bad Gateway: {e}"[:123])
|
|
278
270
|
else:
|
|
279
271
|
logger.error(f"WebSocket Proxy Error: {e}")
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
272
|
+
raise WebSocketException(code=http_codes.WS_1008_POLICY_VIOLATION, reason=f"WebSocket Proxy Error: {e}"[:123]) # max 123 byte
|
|
273
|
+
else:
|
|
274
|
+
if websocket.client_state == WebSocketState.CONNECTED:
|
|
275
|
+
try: await websocket.close(code=http_codes.WS_1012_SERVICE_RESTART)
|
|
276
|
+
except: pass
|
|
277
|
+
raise e
|
|
286
278
|
|
|
287
279
|
|
|
288
280
|
async def _handle_ws_bidirectional(websocket: WebSocket, target_ws):
|
|
@@ -298,9 +290,12 @@ async def _handle_ws_bidirectional(websocket: WebSocket, target_ws):
|
|
|
298
290
|
await target_ws.send(message["bytes"])
|
|
299
291
|
elif message["type"] == "websocket.disconnect":
|
|
300
292
|
break
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
293
|
+
# Exit the loop on error or in cancellation
|
|
294
|
+
except asyncio.CancelledError: pass # Just cancelled, no error
|
|
295
|
+
except Exception as e:
|
|
296
|
+
# Log on Debug level
|
|
297
|
+
logger.debug(f"WS relay (client→target) ended: {e}")
|
|
298
|
+
|
|
304
299
|
|
|
305
300
|
async def target_to_client():
|
|
306
301
|
try:
|
|
@@ -310,9 +305,10 @@ async def _handle_ws_bidirectional(websocket: WebSocket, target_ws):
|
|
|
310
305
|
await websocket.send_text(message)
|
|
311
306
|
else:
|
|
312
307
|
await websocket.send_bytes(message)
|
|
313
|
-
except (
|
|
314
|
-
|
|
315
|
-
|
|
308
|
+
except (asyncio.CancelledError, websockets.ConnectionClosed): pass # cancelled or close -> no error
|
|
309
|
+
except Exception as e:
|
|
310
|
+
# Log on Debug Level
|
|
311
|
+
logger.debug(f"WS relay (target→client) ended: {e}")
|
|
316
312
|
|
|
317
313
|
# Wrap in tasks for cancellation
|
|
318
314
|
tasks = [
|
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-reverse-proxy
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: A robust, streaming-capable reverse proxy for FastAPI including WebSocket support.
|
|
5
|
+
Author-email: Tomás <tomas@suricatingss.xyz>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/tfsantos05/fastapi-reverse-proxy
|
|
8
|
+
Project-URL: Issues, https://github.com/tfsantos05/fastapi-reverse-proxy/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Framework :: FastAPI
|
|
12
|
+
Classifier: Topic :: Internet :: Proxy Servers
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: fastapi>=0.129.0
|
|
17
|
+
Requires-Dist: httpx>=0.28.1
|
|
18
|
+
Requires-Dist: starlette>=0.52.1
|
|
19
|
+
Requires-Dist: uvicorn>=0.41.0
|
|
20
|
+
Requires-Dist: anyio>=4.12.1
|
|
21
|
+
Requires-Dist: certifi>=2026.1.4
|
|
22
|
+
Requires-Dist: pydantic>=2.12.5
|
|
23
|
+
Requires-Dist: url-normalize>=2.2.1
|
|
24
|
+
Requires-Dist: typing_extensions>=4.15.0
|
|
25
|
+
Requires-Dist: websockets>=16.0
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
1
28
|
# FastAPI Reverse Proxy
|
|
2
29
|
|
|
3
30
|
A robust, streaming-capable reverse proxy for FastAPI/Starlette with built-in **Latency-Based Load Balancing** and **Active Health Monitoring**.
|
|
@@ -68,7 +95,7 @@ The `proxy_pass` function and `LoadBalancer.proxy_pass` provide deep customizati
|
|
|
68
95
|
| :--- | :--- | :--- |
|
|
69
96
|
| `timeout` | `float` | Total request timeout in seconds (Default: `60.0`). |
|
|
70
97
|
| `method` | `str` | Force a specific HTTP method (e.g., `"POST"`). |
|
|
71
|
-
| `override_body` | `bytes` |
|
|
98
|
+
| `override_body` | `bytes \| str \| dict \| list` | Use this instead of streaming the request body. `str` is UTF-8 encoded; `dict`/`list` are JSON-serialized automatically (automatically sets `Content-Type: application/json` if not already set). |
|
|
72
99
|
| `additional_headers` | `dict` | Append custom headers to the proxied request. |
|
|
73
100
|
| `override_headers` | `dict` | Use these headers *instead* of original request headers. |
|
|
74
101
|
| `forward_query` | `bool` | Whether to append the incoming query string (Default: `True`). |
|
|
@@ -104,6 +131,8 @@ The library implements "deferred negotiation" for WebSockets:
|
|
|
104
131
|
3. Once the upstream accepts a protocol, the proxy calls `websocket.accept(subprotocol=...)` back to the client.
|
|
105
132
|
4. This ensures the entire tunnel (Client <-> Proxy <-> Upstream) uses the same negotiated protocol.
|
|
106
133
|
5. **Handshake Timeout**: Supports a customizable `timeout` parameter (default `10.0s`) to prevent hangs if the backend is unresponsive.
|
|
134
|
+
6. **Error Handling**: Raises `fastapi.WebSocketException` when the upstream connection fails or is rejected with proper WS codes such as 1008 or 1011
|
|
135
|
+
7. **Debug**: Disruptions from either side are logged on debug level.
|
|
107
136
|
|
|
108
137
|
## Robustness & Safety
|
|
109
138
|
|
|
@@ -114,6 +143,14 @@ The library implements "deferred negotiation" for WebSockets:
|
|
|
114
143
|
`Proxy-Authorization`) are stripped from both outbound requests and responses,
|
|
115
144
|
per spec. WebSocket handshake headers (`Sec-WebSocket-Key`, `Upgrade`, etc.)
|
|
116
145
|
from the client are never forwarded to the target, avoiding handshake collisions.
|
|
146
|
+
- **Content-Length Safety**: Always stripped from the outbound request and
|
|
147
|
+
recalculated by `httpx` (or sent chunked when streaming), preventing
|
|
148
|
+
`LocalProtocolError` when `override_body` differs in size from the original
|
|
149
|
+
request.
|
|
150
|
+
- **Content-Encoding Passthrough**: Compressed upstream responses (`gzip`, `br`,
|
|
151
|
+
etc.) are streamed back to the client raw via `aiter_raw()`, with
|
|
152
|
+
`Content-Encoding` preserved — the client decompresses it itself, avoiding
|
|
153
|
+
corrupted/garbled response bodies.
|
|
117
154
|
|
|
118
155
|
|
|
119
156
|
|
|
@@ -156,3 +193,12 @@ location /ws/ {
|
|
|
156
193
|
|
|
157
194
|
Without this configuration, `X-Forwarded-*` headers will be empty or missing by
|
|
158
195
|
the time they reach your application.
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
### Streaming & Response Buffering
|
|
199
|
+
|
|
200
|
+
`X-Accel-Buffering: no` is set automatically on every proxied response to
|
|
201
|
+
disable Nginx's response buffering for streamed content (SSE, chunked
|
|
202
|
+
transfers, large file downloads). If deploying behind Apache instead, disable
|
|
203
|
+
buffering via your Apache config (`mod_proxy` directives) — there's no
|
|
204
|
+
equivalent response header Apache recognizes.
|
|
File without changes
|
|
File without changes
|
{fastapi_reverse_proxy-0.3.1 → fastapi_reverse_proxy-0.4.0}/src/fastapi_reverse_proxy/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_reverse_proxy-0.3.1 → fastapi_reverse_proxy-0.4.0}/src/fastapi_reverse_proxy/proxy_httpx.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|