fastapi-reverse-proxy 0.1.1__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 (16) hide show
  1. {fastapi_reverse_proxy-0.1.1/src/fastapi_reverse_proxy.egg-info → fastapi_reverse_proxy-0.3.0}/PKG-INFO +37 -25
  2. {fastapi_reverse_proxy-0.1.1 → fastapi_reverse_proxy-0.3.0}/README.md +36 -24
  3. {fastapi_reverse_proxy-0.1.1 → fastapi_reverse_proxy-0.3.0}/pyproject.toml +1 -1
  4. {fastapi_reverse_proxy-0.1.1 → fastapi_reverse_proxy-0.3.0}/src/fastapi_reverse_proxy/__init__.py +2 -1
  5. {fastapi_reverse_proxy-0.1.1 → fastapi_reverse_proxy-0.3.0}/src/fastapi_reverse_proxy/load_balance.py +3 -3
  6. fastapi_reverse_proxy-0.3.0/src/fastapi_reverse_proxy/proxy_httpx.py +30 -0
  7. {fastapi_reverse_proxy-0.1.1 → fastapi_reverse_proxy-0.3.0}/src/fastapi_reverse_proxy/proxy_pass.py +46 -13
  8. {fastapi_reverse_proxy-0.1.1 → fastapi_reverse_proxy-0.3.0/src/fastapi_reverse_proxy.egg-info}/PKG-INFO +37 -25
  9. fastapi_reverse_proxy-0.1.1/src/fastapi_reverse_proxy/proxy_httpx.py +0 -15
  10. {fastapi_reverse_proxy-0.1.1 → fastapi_reverse_proxy-0.3.0}/LICENSE +0 -0
  11. {fastapi_reverse_proxy-0.1.1 → fastapi_reverse_proxy-0.3.0}/setup.cfg +0 -0
  12. {fastapi_reverse_proxy-0.1.1 → fastapi_reverse_proxy-0.3.0}/src/fastapi_reverse_proxy/health_check.py +0 -0
  13. {fastapi_reverse_proxy-0.1.1 → fastapi_reverse_proxy-0.3.0}/src/fastapi_reverse_proxy.egg-info/SOURCES.txt +0 -0
  14. {fastapi_reverse_proxy-0.1.1 → fastapi_reverse_proxy-0.3.0}/src/fastapi_reverse_proxy.egg-info/dependency_links.txt +0 -0
  15. {fastapi_reverse_proxy-0.1.1 → fastapi_reverse_proxy-0.3.0}/src/fastapi_reverse_proxy.egg-info/requires.txt +0 -0
  16. {fastapi_reverse_proxy-0.1.1 → 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.1.1
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
@@ -29,58 +29,69 @@ A robust, streaming-capable reverse proxy for FastAPI/Starlette with built-in **
29
29
 
30
30
  ## Features
31
31
 
32
- - **Streaming Ready**: Efficiently handles SSE (Server-Sent Events) and large file uploads/downloads.
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.
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.
38
41
  - **Version Agnostic**: Automatically handles `websockets` library version differences (12.0+ vs Legacy).
39
42
 
40
- ## Quick Start (Best Practice)
43
+ ## Quick Start
41
44
 
42
- The recommended way to use the library is within a FastAPI **lifespan** handler. This ensures all background monitoring tasks and HTTP clients start and stop cleanly.
45
+ Use the **lifespan** handler as shown for an easy launch.
46
+
47
+ The simplest way to use the proxy is to use **proxy_pass** and/or **proxy_pass_websocket** on the endpoints.
43
48
 
44
49
  ```python
45
50
  from fastapi import FastAPI, Request, WebSocket
46
51
  from contextlib import asynccontextmanager
47
52
 
48
- from fastapi_reverse_proxy import (
49
- HealthChecker, LoadBalancer,
50
- create_httpx_client, close_httpx_client
51
- )
52
-
53
- # 1. Setup health monitoring and load balancing
54
- checker = HealthChecker(["http://localhost:8080", "http://localhost:8081"])
55
- lb = LoadBalancer(checker)
53
+ from fastapi_reverse_proxy import Proxy, proxy_pass, proxy_pass_websocket
56
54
 
57
55
  @asynccontextmanager
58
56
  async def lifespan(app: FastAPI):
59
- # Initialize global resources
60
- await create_httpx_client(app)
61
- async with checker: # Starts background health loop
57
+ async with Proxy(app):
62
58
  yield
63
- await close_httpx_client(app)
64
59
 
65
60
  app = FastAPI(lifespan=lifespan)
66
61
 
67
- @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
68
- async def gateway(request: Request, path: str):
69
- # Route to the fastest healthy backend
70
- return await lb.proxy_pass(request, path=f"/{path}")
62
+ # catch-all route. recommended for a reverse proxy
63
+ @app.api_route("/{path:path}", methods=["GET","POST","PUT","DELETE"]) # don't forget to add the methods.
64
+ async def index(req: Request):
65
+ """
66
+ You always need to pass the "Request" object and to specify the host
67
+ If you don't add a path, it will be the same as the original (/login --> http://127.0.0.1/login)
68
+ """
69
+ return await proxy_pass(req, "http://127.0.0.1:8080")
71
70
 
72
- @app.websocket("/ws/{path:path}")
73
- async def ws_tunnel(websocket: WebSocket, path: str):
74
- # Automatic subprotocol negotiation + Tunneling
75
- await lb.proxy_pass_websocket(websocket, path=f"/{path}")
76
71
  ```
77
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
+
81
+ ## Advanced Examples:
82
+
83
+ Check [examples](https://github.com/tfsantos05/fastapi-reverse-proxy/tree/main/examples) for full examples, including:
84
+ - **Websocket Proxy**
85
+ - **Socket.IO Proxy**
86
+ - **Error Handling & Failover** (`examples/error_handling_example.py`)
87
+
78
88
  ## Advanced Proxying
79
89
 
80
90
  The `proxy_pass` function and `LoadBalancer.proxy_pass` provide deep customization for upstream requests:
81
91
 
82
92
  | Parameter | Type | Description |
83
93
  | :--- | :--- | :--- |
94
+ | `timeout` | `float` | Total request timeout in seconds (Default: `60.0`). |
84
95
  | `method` | `str` | Force a specific HTTP method (e.g., `"POST"`). |
85
96
  | `override_body` | `bytes` | Send custom data instead of the incoming request body. |
86
97
  | `additional_headers` | `dict` | Append custom headers to the proxied request. |
@@ -116,6 +127,7 @@ The library implements "deferred negotiation" for WebSockets:
116
127
  2. It establishes an upstream connection first.
117
128
  3. Once the upstream accepts a protocol, the proxy calls `websocket.accept(subprotocol=...)` back to the client.
118
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.
119
131
 
120
132
  ## Robustness & Safety
121
133
 
@@ -4,58 +4,69 @@ A robust, streaming-capable reverse proxy for FastAPI/Starlette with built-in **
4
4
 
5
5
  ## Features
6
6
 
7
- - **Streaming Ready**: Efficiently handles SSE (Server-Sent Events) and large file uploads/downloads.
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.
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.
13
16
  - **Version Agnostic**: Automatically handles `websockets` library version differences (12.0+ vs Legacy).
14
17
 
15
- ## Quick Start (Best Practice)
18
+ ## Quick Start
16
19
 
17
- The recommended way to use the library is within a FastAPI **lifespan** handler. This ensures all background monitoring tasks and HTTP clients start and stop cleanly.
20
+ Use the **lifespan** handler as shown for an easy launch.
21
+
22
+ The simplest way to use the proxy is to use **proxy_pass** and/or **proxy_pass_websocket** on the endpoints.
18
23
 
19
24
  ```python
20
25
  from fastapi import FastAPI, Request, WebSocket
21
26
  from contextlib import asynccontextmanager
22
27
 
23
- from fastapi_reverse_proxy import (
24
- HealthChecker, LoadBalancer,
25
- create_httpx_client, close_httpx_client
26
- )
27
-
28
- # 1. Setup health monitoring and load balancing
29
- checker = HealthChecker(["http://localhost:8080", "http://localhost:8081"])
30
- lb = LoadBalancer(checker)
28
+ from fastapi_reverse_proxy import Proxy, proxy_pass, proxy_pass_websocket
31
29
 
32
30
  @asynccontextmanager
33
31
  async def lifespan(app: FastAPI):
34
- # Initialize global resources
35
- await create_httpx_client(app)
36
- async with checker: # Starts background health loop
32
+ async with Proxy(app):
37
33
  yield
38
- await close_httpx_client(app)
39
34
 
40
35
  app = FastAPI(lifespan=lifespan)
41
36
 
42
- @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
43
- async def gateway(request: Request, path: str):
44
- # Route to the fastest healthy backend
45
- return await lb.proxy_pass(request, path=f"/{path}")
37
+ # catch-all route. recommended for a reverse proxy
38
+ @app.api_route("/{path:path}", methods=["GET","POST","PUT","DELETE"]) # don't forget to add the methods.
39
+ async def index(req: Request):
40
+ """
41
+ You always need to pass the "Request" object and to specify the host
42
+ If you don't add a path, it will be the same as the original (/login --> http://127.0.0.1/login)
43
+ """
44
+ return await proxy_pass(req, "http://127.0.0.1:8080")
46
45
 
47
- @app.websocket("/ws/{path:path}")
48
- async def ws_tunnel(websocket: WebSocket, path: str):
49
- # Automatic subprotocol negotiation + Tunneling
50
- await lb.proxy_pass_websocket(websocket, path=f"/{path}")
51
46
  ```
52
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
+
56
+ ## Advanced Examples:
57
+
58
+ Check [examples](https://github.com/tfsantos05/fastapi-reverse-proxy/tree/main/examples) for full examples, including:
59
+ - **Websocket Proxy**
60
+ - **Socket.IO Proxy**
61
+ - **Error Handling & Failover** (`examples/error_handling_example.py`)
62
+
53
63
  ## Advanced Proxying
54
64
 
55
65
  The `proxy_pass` function and `LoadBalancer.proxy_pass` provide deep customization for upstream requests:
56
66
 
57
67
  | Parameter | Type | Description |
58
68
  | :--- | :--- | :--- |
69
+ | `timeout` | `float` | Total request timeout in seconds (Default: `60.0`). |
59
70
  | `method` | `str` | Force a specific HTTP method (e.g., `"POST"`). |
60
71
  | `override_body` | `bytes` | Send custom data instead of the incoming request body. |
61
72
  | `additional_headers` | `dict` | Append custom headers to the proxied request. |
@@ -91,6 +102,7 @@ The library implements "deferred negotiation" for WebSockets:
91
102
  2. It establishes an upstream connection first.
92
103
  3. Once the upstream accepts a protocol, the proxy calls `websocket.accept(subprotocol=...)` back to the client.
93
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.
94
106
 
95
107
  ## Robustness & Safety
96
108
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fastapi-reverse-proxy"
7
- version = "0.1.1"
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
  ]
@@ -158,12 +158,12 @@ class LoadBalancer:
158
158
 
159
159
  u = urlparse(target)
160
160
  origin = f"{u.scheme}://{u.netloc}"
161
- # Smart pathing: combine origin and user-provided path (or default path)
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
- dest_url,
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
@@ -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
@@ -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,21 @@ 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
+ 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
+
22
38
  async def proxy_pass(
23
39
  request: Request,
24
40
  host: str,
@@ -33,7 +49,7 @@ async def proxy_pass(
33
49
  """
34
50
  Forwards incoming HTTP requests to the target service using streaming.
35
51
  - host: The host itself (without ending slash)
36
- - path: The path with beggining slash
52
+ - path: The path w/ beggining slash (by default copies requests's path)
37
53
  - forward_query: If True, automatically appends the request's query string.
38
54
  - additional_headers: Headers to add to the upstream request.
39
55
  - override_headers: Use these headers instead of original request headers.
@@ -146,25 +162,35 @@ async def proxy_pass(
146
162
  # Catch EVERY exception (including CancelledError) for local client cleanup
147
163
  if not is_global_client and client:
148
164
  await client.aclose()
149
- # 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
+
150
170
  raise e
151
171
 
152
172
 
153
-
154
173
  async def proxy_pass_websocket(
155
174
  websocket: WebSocket,
156
- target_url: str,
175
+ host: str,
176
+ path: Optional[str] = None,
157
177
  subprotocols: Optional[list[str]] = None,
158
178
  forward_query: bool = True,
159
179
  additional_headers: Optional[dict] = None,
160
- override_headers: Optional[dict] = None
180
+ override_headers: Optional[dict] = None,
181
+ timeout: float = 10.0
161
182
  ):
162
183
  """
163
184
  Forwards incoming WebSocket connections to the target service.
164
- - target_url: The full destination WS(S) URL.
185
+ - host: The host itself (without ending slash)
186
+ - path: The path with beggining slash (by default copies requests's path)
165
187
  - forward_query: If True, automatically appends the request's query string.
188
+ - timeout: Time to wait for the connection handshake (open_timeout).
166
189
  """
167
- url = target_url
190
+
191
+ if path is None: path = websocket.url.path
192
+ url = url_normalize_ws(host + path)
193
+
168
194
  if forward_query and websocket.url.query:
169
195
  url = f"{url}?{websocket.url.query}" if "?" not in url else f"{url}&{websocket.url.query}"
170
196
 
@@ -200,7 +226,8 @@ async def proxy_pass_websocket(
200
226
 
201
227
  connect_kwargs = {
202
228
  header_param: headers,
203
- "subprotocols": supported_subprotocols
229
+ "subprotocols": supported_subprotocols,
230
+ "open_timeout": timeout
204
231
  }
205
232
 
206
233
  async with websockets.connect(url, **connect_kwargs) as target_ws:
@@ -210,7 +237,16 @@ async def proxy_pass_websocket(
210
237
 
211
238
  except BaseException as e:
212
239
  if not isinstance(e, asyncio.CancelledError):
213
- 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}")
214
250
  raise e
215
251
  finally:
216
252
  try:
@@ -219,9 +255,6 @@ async def proxy_pass_websocket(
219
255
  pass
220
256
 
221
257
 
222
-
223
-
224
-
225
258
  async def _handle_ws_bidirectional(websocket: WebSocket, target_ws):
226
259
  """Internal helper to manage bidirectional WS traffic with clean cancellation."""
227
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.1.1
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
@@ -29,58 +29,69 @@ A robust, streaming-capable reverse proxy for FastAPI/Starlette with built-in **
29
29
 
30
30
  ## Features
31
31
 
32
- - **Streaming Ready**: Efficiently handles SSE (Server-Sent Events) and large file uploads/downloads.
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.
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.
38
41
  - **Version Agnostic**: Automatically handles `websockets` library version differences (12.0+ vs Legacy).
39
42
 
40
- ## Quick Start (Best Practice)
43
+ ## Quick Start
41
44
 
42
- The recommended way to use the library is within a FastAPI **lifespan** handler. This ensures all background monitoring tasks and HTTP clients start and stop cleanly.
45
+ Use the **lifespan** handler as shown for an easy launch.
46
+
47
+ The simplest way to use the proxy is to use **proxy_pass** and/or **proxy_pass_websocket** on the endpoints.
43
48
 
44
49
  ```python
45
50
  from fastapi import FastAPI, Request, WebSocket
46
51
  from contextlib import asynccontextmanager
47
52
 
48
- from fastapi_reverse_proxy import (
49
- HealthChecker, LoadBalancer,
50
- create_httpx_client, close_httpx_client
51
- )
52
-
53
- # 1. Setup health monitoring and load balancing
54
- checker = HealthChecker(["http://localhost:8080", "http://localhost:8081"])
55
- lb = LoadBalancer(checker)
53
+ from fastapi_reverse_proxy import Proxy, proxy_pass, proxy_pass_websocket
56
54
 
57
55
  @asynccontextmanager
58
56
  async def lifespan(app: FastAPI):
59
- # Initialize global resources
60
- await create_httpx_client(app)
61
- async with checker: # Starts background health loop
57
+ async with Proxy(app):
62
58
  yield
63
- await close_httpx_client(app)
64
59
 
65
60
  app = FastAPI(lifespan=lifespan)
66
61
 
67
- @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
68
- async def gateway(request: Request, path: str):
69
- # Route to the fastest healthy backend
70
- return await lb.proxy_pass(request, path=f"/{path}")
62
+ # catch-all route. recommended for a reverse proxy
63
+ @app.api_route("/{path:path}", methods=["GET","POST","PUT","DELETE"]) # don't forget to add the methods.
64
+ async def index(req: Request):
65
+ """
66
+ You always need to pass the "Request" object and to specify the host
67
+ If you don't add a path, it will be the same as the original (/login --> http://127.0.0.1/login)
68
+ """
69
+ return await proxy_pass(req, "http://127.0.0.1:8080")
71
70
 
72
- @app.websocket("/ws/{path:path}")
73
- async def ws_tunnel(websocket: WebSocket, path: str):
74
- # Automatic subprotocol negotiation + Tunneling
75
- await lb.proxy_pass_websocket(websocket, path=f"/{path}")
76
71
  ```
77
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
+
81
+ ## Advanced Examples:
82
+
83
+ Check [examples](https://github.com/tfsantos05/fastapi-reverse-proxy/tree/main/examples) for full examples, including:
84
+ - **Websocket Proxy**
85
+ - **Socket.IO Proxy**
86
+ - **Error Handling & Failover** (`examples/error_handling_example.py`)
87
+
78
88
  ## Advanced Proxying
79
89
 
80
90
  The `proxy_pass` function and `LoadBalancer.proxy_pass` provide deep customization for upstream requests:
81
91
 
82
92
  | Parameter | Type | Description |
83
93
  | :--- | :--- | :--- |
94
+ | `timeout` | `float` | Total request timeout in seconds (Default: `60.0`). |
84
95
  | `method` | `str` | Force a specific HTTP method (e.g., `"POST"`). |
85
96
  | `override_body` | `bytes` | Send custom data instead of the incoming request body. |
86
97
  | `additional_headers` | `dict` | Append custom headers to the proxied request. |
@@ -116,6 +127,7 @@ The library implements "deferred negotiation" for WebSockets:
116
127
  2. It establishes an upstream connection first.
117
128
  3. Once the upstream accepts a protocol, the proxy calls `websocket.accept(subprotocol=...)` back to the client.
118
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.
119
131
 
120
132
  ## Robustness & Safety
121
133
 
@@ -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