fastapi-reverse-proxy 0.2.0__tar.gz → 0.3.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.
Files changed (15) hide show
  1. {fastapi_reverse_proxy-0.2.0/src/fastapi_reverse_proxy.egg-info → fastapi_reverse_proxy-0.3.0}/PKG-INFO +15 -2
  2. {fastapi_reverse_proxy-0.2.0 → fastapi_reverse_proxy-0.3.0}/README.md +14 -1
  3. {fastapi_reverse_proxy-0.2.0 → fastapi_reverse_proxy-0.3.0}/pyproject.toml +1 -1
  4. {fastapi_reverse_proxy-0.2.0 → fastapi_reverse_proxy-0.3.0}/src/fastapi_reverse_proxy/__init__.py +2 -1
  5. {fastapi_reverse_proxy-0.2.0 → fastapi_reverse_proxy-0.3.0}/src/fastapi_reverse_proxy/proxy_pass.py +32 -10
  6. {fastapi_reverse_proxy-0.2.0 → fastapi_reverse_proxy-0.3.0/src/fastapi_reverse_proxy.egg-info}/PKG-INFO +15 -2
  7. {fastapi_reverse_proxy-0.2.0 → fastapi_reverse_proxy-0.3.0}/LICENSE +0 -0
  8. {fastapi_reverse_proxy-0.2.0 → fastapi_reverse_proxy-0.3.0}/setup.cfg +0 -0
  9. {fastapi_reverse_proxy-0.2.0 → fastapi_reverse_proxy-0.3.0}/src/fastapi_reverse_proxy/health_check.py +0 -0
  10. {fastapi_reverse_proxy-0.2.0 → fastapi_reverse_proxy-0.3.0}/src/fastapi_reverse_proxy/load_balance.py +0 -0
  11. {fastapi_reverse_proxy-0.2.0 → fastapi_reverse_proxy-0.3.0}/src/fastapi_reverse_proxy/proxy_httpx.py +0 -0
  12. {fastapi_reverse_proxy-0.2.0 → fastapi_reverse_proxy-0.3.0}/src/fastapi_reverse_proxy.egg-info/SOURCES.txt +0 -0
  13. {fastapi_reverse_proxy-0.2.0 → fastapi_reverse_proxy-0.3.0}/src/fastapi_reverse_proxy.egg-info/dependency_links.txt +0 -0
  14. {fastapi_reverse_proxy-0.2.0 → fastapi_reverse_proxy-0.3.0}/src/fastapi_reverse_proxy.egg-info/requires.txt +0 -0
  15. {fastapi_reverse_proxy-0.2.0 → fastapi_reverse_proxy-0.3.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.2.0
3
+ Version: 0.3.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
@@ -36,6 +36,8 @@ A robust, streaming-capable reverse proxy for FastAPI/Starlette with built-in **
36
36
  - **Unified Load Balancing**: Standard Round-Robin or Smart routing using a single utility.
37
37
  - **Latency-Based Routing**: Automatically routes traffic to the fastest healthy server (HEAD probe).
38
38
  - **Advanced Overrides**: Granular control over headers, body, and HTTP methods.
39
+ - **Smart Error Mapping**: Automatically converts upstream connection failures into standard HTTP 502 (Bad Gateway) and 504 (Gateway Timeout) responses.
40
+ - **Resilient Handshakes**: Customizable `open_timeout` for WebSockets to prevent proxy hangs during backend connection attempts.
39
41
  - **Version Agnostic**: Automatically handles `websockets` library version differences (12.0+ vs Legacy).
40
42
 
41
43
  ## Quick Start
@@ -68,11 +70,20 @@ async def index(req: Request):
68
70
 
69
71
  ```
70
72
 
73
+ ## 🛡️ Resilience & Error Handling
74
+
75
+ Error Handlingfastapi-reverse-proxy transforms upstream crashes into meaningful HTTPException responses (e.g., 502 Bad Gateway or 504 Gateway Timeout).
76
+
77
+ This allows you to implement custom failover logic, retry mechanisms, or specific error pages.
78
+
79
+ For a full implementation of a primary-to-backup failover system, see the [example](https://github.com/tfsantos05/fastapi-reverse-proxy/tree/main/examples/02_http_errors.py).
80
+
71
81
  ## Advanced Examples:
72
82
 
73
- Check [example.py](example.py) for full examples, including
83
+ Check [examples](https://github.com/tfsantos05/fastapi-reverse-proxy/tree/main/examples) for full examples, including:
74
84
  - **Websocket Proxy**
75
85
  - **Socket.IO Proxy**
86
+ - **Error Handling & Failover** (`examples/error_handling_example.py`)
76
87
 
77
88
  ## Advanced Proxying
78
89
 
@@ -80,6 +91,7 @@ The `proxy_pass` function and `LoadBalancer.proxy_pass` provide deep customizati
80
91
 
81
92
  | Parameter | Type | Description |
82
93
  | :--- | :--- | :--- |
94
+ | `timeout` | `float` | Total request timeout in seconds (Default: `60.0`). |
83
95
  | `method` | `str` | Force a specific HTTP method (e.g., `"POST"`). |
84
96
  | `override_body` | `bytes` | Send custom data instead of the incoming request body. |
85
97
  | `additional_headers` | `dict` | Append custom headers to the proxied request. |
@@ -115,6 +127,7 @@ The library implements "deferred negotiation" for WebSockets:
115
127
  2. It establishes an upstream connection first.
116
128
  3. Once the upstream accepts a protocol, the proxy calls `websocket.accept(subprotocol=...)` back to the client.
117
129
  4. This ensures the entire tunnel (Client <-> Proxy <-> Upstream) uses the same negotiated protocol.
130
+ 5. **Handshake Timeout**: Supports a customizable `timeout` parameter (default `10.0s`) to prevent hangs if the backend is unresponsive.
118
131
 
119
132
  ## Robustness & Safety
120
133
 
@@ -11,6 +11,8 @@ A robust, streaming-capable reverse proxy for FastAPI/Starlette with built-in **
11
11
  - **Unified Load Balancing**: Standard Round-Robin or Smart routing using a single utility.
12
12
  - **Latency-Based Routing**: Automatically routes traffic to the fastest healthy server (HEAD probe).
13
13
  - **Advanced Overrides**: Granular control over headers, body, and HTTP methods.
14
+ - **Smart Error Mapping**: Automatically converts upstream connection failures into standard HTTP 502 (Bad Gateway) and 504 (Gateway Timeout) responses.
15
+ - **Resilient Handshakes**: Customizable `open_timeout` for WebSockets to prevent proxy hangs during backend connection attempts.
14
16
  - **Version Agnostic**: Automatically handles `websockets` library version differences (12.0+ vs Legacy).
15
17
 
16
18
  ## Quick Start
@@ -43,11 +45,20 @@ async def index(req: Request):
43
45
 
44
46
  ```
45
47
 
48
+ ## 🛡️ Resilience & Error Handling
49
+
50
+ Error Handlingfastapi-reverse-proxy transforms upstream crashes into meaningful HTTPException responses (e.g., 502 Bad Gateway or 504 Gateway Timeout).
51
+
52
+ This allows you to implement custom failover logic, retry mechanisms, or specific error pages.
53
+
54
+ For a full implementation of a primary-to-backup failover system, see the [example](https://github.com/tfsantos05/fastapi-reverse-proxy/tree/main/examples/02_http_errors.py).
55
+
46
56
  ## Advanced Examples:
47
57
 
48
- Check [example.py](example.py) for full examples, including
58
+ Check [examples](https://github.com/tfsantos05/fastapi-reverse-proxy/tree/main/examples) for full examples, including:
49
59
  - **Websocket Proxy**
50
60
  - **Socket.IO Proxy**
61
+ - **Error Handling & Failover** (`examples/error_handling_example.py`)
51
62
 
52
63
  ## Advanced Proxying
53
64
 
@@ -55,6 +66,7 @@ The `proxy_pass` function and `LoadBalancer.proxy_pass` provide deep customizati
55
66
 
56
67
  | Parameter | Type | Description |
57
68
  | :--- | :--- | :--- |
69
+ | `timeout` | `float` | Total request timeout in seconds (Default: `60.0`). |
58
70
  | `method` | `str` | Force a specific HTTP method (e.g., `"POST"`). |
59
71
  | `override_body` | `bytes` | Send custom data instead of the incoming request body. |
60
72
  | `additional_headers` | `dict` | Append custom headers to the proxied request. |
@@ -90,6 +102,7 @@ The library implements "deferred negotiation" for WebSockets:
90
102
  2. It establishes an upstream connection first.
91
103
  3. Once the upstream accepts a protocol, the proxy calls `websocket.accept(subprotocol=...)` back to the client.
92
104
  4. This ensures the entire tunnel (Client <-> Proxy <-> Upstream) uses the same negotiated protocol.
105
+ 5. **Handshake Timeout**: Supports a customizable `timeout` parameter (default `10.0s`) to prevent hangs if the backend is unresponsive.
93
106
 
94
107
  ## Robustness & Safety
95
108
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fastapi-reverse-proxy"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  authors = [
9
9
  { name="Tomás", email="tomas@suricatingss.xyz" },
10
10
  ]
@@ -1,5 +1,5 @@
1
1
  from .proxy_pass import proxy_pass, proxy_pass_websocket
2
- from .proxy_httpx import create_httpx_client, close_httpx_client, get_httpx_client
2
+ from .proxy_httpx import create_httpx_client, close_httpx_client, get_httpx_client, Proxy
3
3
  from .load_balance import LoadBalancer
4
4
  from .health_check import HealthChecker
5
5
 
@@ -9,6 +9,7 @@ __all__ = [
9
9
  "create_httpx_client",
10
10
  "close_httpx_client",
11
11
  "get_httpx_client",
12
+ "Proxy",
12
13
  "LoadBalancer",
13
14
  "HealthChecker",
14
15
  ]
@@ -1,4 +1,4 @@
1
- from fastapi import Request, WebSocket, Response
1
+ from fastapi import Request, WebSocket, Response, HTTPException
2
2
  from fastapi.responses import StreamingResponse
3
3
  from starlette.background import BackgroundTask
4
4
  from url_normalize import url_normalize
@@ -23,8 +23,18 @@ EXCLUDED_HEADERS = {
23
23
  def url_normalize_ws(url:str):
24
24
  u = urlparse(url)
25
25
  return url_normalize(url.replace(u.scheme, "http", 1)).replace("http", u.scheme, 1)
26
-
27
26
 
27
+ async def handle_proxy_exception(e: Exception):
28
+ """Maps internal exceptions to FastAPI HTTPExceptions for the client."""
29
+ if isinstance(e, httpx.ConnectTimeout):
30
+ raise HTTPException(status_code=504, detail="Gateway Timeout")
31
+ if isinstance(e, (httpx.ConnectError, httpx.RemoteProtocolError)):
32
+ raise HTTPException(status_code=502, detail="Bad Gateway")
33
+ if isinstance(e, httpx.ReadTimeout):
34
+ raise HTTPException(status_code=504, detail="Upstream Read Timeout")
35
+ # Re-raise other exceptions (like CancelledError or internal bugs) to avoid masking
36
+ raise e
37
+
28
38
  async def proxy_pass(
29
39
  request: Request,
30
40
  host: str,
@@ -152,11 +162,14 @@ async def proxy_pass(
152
162
  # Catch EVERY exception (including CancelledError) for local client cleanup
153
163
  if not is_global_client and client:
154
164
  await client.aclose()
155
- # Re-raise so the server can handle the cancellation/error
165
+
166
+ # Transform httpx errors into proper HTTP responses
167
+ if not isinstance(e, asyncio.CancelledError):
168
+ await handle_proxy_exception(e)
169
+
156
170
  raise e
157
171
 
158
172
 
159
-
160
173
  async def proxy_pass_websocket(
161
174
  websocket: WebSocket,
162
175
  host: str,
@@ -164,13 +177,15 @@ async def proxy_pass_websocket(
164
177
  subprotocols: Optional[list[str]] = None,
165
178
  forward_query: bool = True,
166
179
  additional_headers: Optional[dict] = None,
167
- override_headers: Optional[dict] = None
180
+ override_headers: Optional[dict] = None,
181
+ timeout: float = 10.0
168
182
  ):
169
183
  """
170
184
  Forwards incoming WebSocket connections to the target service.
171
185
  - host: The host itself (without ending slash)
172
186
  - path: The path with beggining slash (by default copies requests's path)
173
187
  - forward_query: If True, automatically appends the request's query string.
188
+ - timeout: Time to wait for the connection handshake (open_timeout).
174
189
  """
175
190
 
176
191
  if path is None: path = websocket.url.path
@@ -211,7 +226,8 @@ async def proxy_pass_websocket(
211
226
 
212
227
  connect_kwargs = {
213
228
  header_param: headers,
214
- "subprotocols": supported_subprotocols
229
+ "subprotocols": supported_subprotocols,
230
+ "open_timeout": timeout
215
231
  }
216
232
 
217
233
  async with websockets.connect(url, **connect_kwargs) as target_ws:
@@ -221,7 +237,16 @@ async def proxy_pass_websocket(
221
237
 
222
238
  except BaseException as e:
223
239
  if not isinstance(e, asyncio.CancelledError):
224
- logger.error(f"WebSocket Proxy Error: {e}")
240
+ # If the connection fails before accept(), we can raise a proper 502
241
+ if not websocket.client.connected: # Roughly checking if handshake finished
242
+ logger.error(f"WebSocket Connection Error: {e}")
243
+ # This is a bit tricky in WS, but if we haven't accepted yet, we can raise
244
+ try:
245
+ raise HTTPException(status_code=502, detail="Bad Gateway: WebSocket connection failed")
246
+ except RuntimeError: # If already accepted, we can't raise HTTPException
247
+ pass
248
+ else:
249
+ logger.error(f"WebSocket Proxy Error: {e}")
225
250
  raise e
226
251
  finally:
227
252
  try:
@@ -230,9 +255,6 @@ async def proxy_pass_websocket(
230
255
  pass
231
256
 
232
257
 
233
-
234
-
235
-
236
258
  async def _handle_ws_bidirectional(websocket: WebSocket, target_ws):
237
259
  """Internal helper to manage bidirectional WS traffic with clean cancellation."""
238
260
  async def client_to_target():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-reverse-proxy
3
- Version: 0.2.0
3
+ Version: 0.3.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
@@ -36,6 +36,8 @@ A robust, streaming-capable reverse proxy for FastAPI/Starlette with built-in **
36
36
  - **Unified Load Balancing**: Standard Round-Robin or Smart routing using a single utility.
37
37
  - **Latency-Based Routing**: Automatically routes traffic to the fastest healthy server (HEAD probe).
38
38
  - **Advanced Overrides**: Granular control over headers, body, and HTTP methods.
39
+ - **Smart Error Mapping**: Automatically converts upstream connection failures into standard HTTP 502 (Bad Gateway) and 504 (Gateway Timeout) responses.
40
+ - **Resilient Handshakes**: Customizable `open_timeout` for WebSockets to prevent proxy hangs during backend connection attempts.
39
41
  - **Version Agnostic**: Automatically handles `websockets` library version differences (12.0+ vs Legacy).
40
42
 
41
43
  ## Quick Start
@@ -68,11 +70,20 @@ async def index(req: Request):
68
70
 
69
71
  ```
70
72
 
73
+ ## 🛡️ Resilience & Error Handling
74
+
75
+ Error Handlingfastapi-reverse-proxy transforms upstream crashes into meaningful HTTPException responses (e.g., 502 Bad Gateway or 504 Gateway Timeout).
76
+
77
+ This allows you to implement custom failover logic, retry mechanisms, or specific error pages.
78
+
79
+ For a full implementation of a primary-to-backup failover system, see the [example](https://github.com/tfsantos05/fastapi-reverse-proxy/tree/main/examples/02_http_errors.py).
80
+
71
81
  ## Advanced Examples:
72
82
 
73
- Check [example.py](example.py) for full examples, including
83
+ Check [examples](https://github.com/tfsantos05/fastapi-reverse-proxy/tree/main/examples) for full examples, including:
74
84
  - **Websocket Proxy**
75
85
  - **Socket.IO Proxy**
86
+ - **Error Handling & Failover** (`examples/error_handling_example.py`)
76
87
 
77
88
  ## Advanced Proxying
78
89
 
@@ -80,6 +91,7 @@ The `proxy_pass` function and `LoadBalancer.proxy_pass` provide deep customizati
80
91
 
81
92
  | Parameter | Type | Description |
82
93
  | :--- | :--- | :--- |
94
+ | `timeout` | `float` | Total request timeout in seconds (Default: `60.0`). |
83
95
  | `method` | `str` | Force a specific HTTP method (e.g., `"POST"`). |
84
96
  | `override_body` | `bytes` | Send custom data instead of the incoming request body. |
85
97
  | `additional_headers` | `dict` | Append custom headers to the proxied request. |
@@ -115,6 +127,7 @@ The library implements "deferred negotiation" for WebSockets:
115
127
  2. It establishes an upstream connection first.
116
128
  3. Once the upstream accepts a protocol, the proxy calls `websocket.accept(subprotocol=...)` back to the client.
117
129
  4. This ensures the entire tunnel (Client <-> Proxy <-> Upstream) uses the same negotiated protocol.
130
+ 5. **Handshake Timeout**: Supports a customizable `timeout` parameter (default `10.0s`) to prevent hangs if the backend is unresponsive.
118
131
 
119
132
  ## Robustness & Safety
120
133