fastapi-reverse-proxy 0.1.1__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.
Files changed (16) hide show
  1. {fastapi_reverse_proxy-0.1.1/src/fastapi_reverse_proxy.egg-info → fastapi_reverse_proxy-0.2.0}/PKG-INFO +24 -25
  2. {fastapi_reverse_proxy-0.1.1 → fastapi_reverse_proxy-0.2.0}/README.md +23 -24
  3. {fastapi_reverse_proxy-0.1.1 → fastapi_reverse_proxy-0.2.0}/pyproject.toml +1 -1
  4. {fastapi_reverse_proxy-0.1.1 → fastapi_reverse_proxy-0.2.0}/src/fastapi_reverse_proxy/load_balance.py +3 -3
  5. fastapi_reverse_proxy-0.2.0/src/fastapi_reverse_proxy/proxy_httpx.py +30 -0
  6. {fastapi_reverse_proxy-0.1.1 → fastapi_reverse_proxy-0.2.0}/src/fastapi_reverse_proxy/proxy_pass.py +15 -4
  7. {fastapi_reverse_proxy-0.1.1 → fastapi_reverse_proxy-0.2.0/src/fastapi_reverse_proxy.egg-info}/PKG-INFO +24 -25
  8. fastapi_reverse_proxy-0.1.1/src/fastapi_reverse_proxy/proxy_httpx.py +0 -15
  9. {fastapi_reverse_proxy-0.1.1 → fastapi_reverse_proxy-0.2.0}/LICENSE +0 -0
  10. {fastapi_reverse_proxy-0.1.1 → fastapi_reverse_proxy-0.2.0}/setup.cfg +0 -0
  11. {fastapi_reverse_proxy-0.1.1 → fastapi_reverse_proxy-0.2.0}/src/fastapi_reverse_proxy/__init__.py +0 -0
  12. {fastapi_reverse_proxy-0.1.1 → fastapi_reverse_proxy-0.2.0}/src/fastapi_reverse_proxy/health_check.py +0 -0
  13. {fastapi_reverse_proxy-0.1.1 → fastapi_reverse_proxy-0.2.0}/src/fastapi_reverse_proxy.egg-info/SOURCES.txt +0 -0
  14. {fastapi_reverse_proxy-0.1.1 → fastapi_reverse_proxy-0.2.0}/src/fastapi_reverse_proxy.egg-info/dependency_links.txt +0 -0
  15. {fastapi_reverse_proxy-0.1.1 → fastapi_reverse_proxy-0.2.0}/src/fastapi_reverse_proxy.egg-info/requires.txt +0 -0
  16. {fastapi_reverse_proxy-0.1.1 → 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.1.1
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,52 +29,51 @@ 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.
38
39
  - **Version Agnostic**: Automatically handles `websockets` library version differences (12.0+ vs Legacy).
39
40
 
40
- ## Quick Start (Best Practice)
41
+ ## Quick Start
41
42
 
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.
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
- 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)
51
+ from fastapi_reverse_proxy import Proxy, proxy_pass, proxy_pass_websocket
56
52
 
57
53
  @asynccontextmanager
58
54
  async def lifespan(app: FastAPI):
59
- # Initialize global resources
60
- await create_httpx_client(app)
61
- async with checker: # Starts background health loop
55
+ async with Proxy(app):
62
56
  yield
63
- await close_httpx_client(app)
64
57
 
65
58
  app = FastAPI(lifespan=lifespan)
66
59
 
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}")
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")
71
68
 
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
69
  ```
77
70
 
71
+ ## Advanced Examples:
72
+
73
+ Check [example.py](example.py) for full examples, including
74
+ - **Websocket Proxy**
75
+ - **Socket.IO Proxy**
76
+
78
77
  ## Advanced Proxying
79
78
 
80
79
  The `proxy_pass` function and `LoadBalancer.proxy_pass` provide deep customization for upstream requests:
@@ -4,52 +4,51 @@ 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.
13
14
  - **Version Agnostic**: Automatically handles `websockets` library version differences (12.0+ vs Legacy).
14
15
 
15
- ## Quick Start (Best Practice)
16
+ ## Quick Start
16
17
 
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.
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
- 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)
26
+ from fastapi_reverse_proxy import Proxy, proxy_pass, proxy_pass_websocket
31
27
 
32
28
  @asynccontextmanager
33
29
  async def lifespan(app: FastAPI):
34
- # Initialize global resources
35
- await create_httpx_client(app)
36
- async with checker: # Starts background health loop
30
+ async with Proxy(app):
37
31
  yield
38
- await close_httpx_client(app)
39
32
 
40
33
  app = FastAPI(lifespan=lifespan)
41
34
 
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}")
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")
46
43
 
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
44
  ```
52
45
 
46
+ ## Advanced Examples:
47
+
48
+ Check [example.py](example.py) for full examples, including
49
+ - **Websocket Proxy**
50
+ - **Socket.IO Proxy**
51
+
53
52
  ## Advanced Proxying
54
53
 
55
54
  The `proxy_pass` function and `LoadBalancer.proxy_pass` provide deep customization for upstream requests:
@@ -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.2.0"
8
8
  authors = [
9
9
  { name="Tomás", email="tomas@suricatingss.xyz" },
10
10
  ]
@@ -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
@@ -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 with beggining slash
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
- target_url: str,
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
- - target_url: The full destination WS(S) URL.
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
- url = target_url
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.1.1
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,52 +29,51 @@ 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.
38
39
  - **Version Agnostic**: Automatically handles `websockets` library version differences (12.0+ vs Legacy).
39
40
 
40
- ## Quick Start (Best Practice)
41
+ ## Quick Start
41
42
 
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.
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
- 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)
51
+ from fastapi_reverse_proxy import Proxy, proxy_pass, proxy_pass_websocket
56
52
 
57
53
  @asynccontextmanager
58
54
  async def lifespan(app: FastAPI):
59
- # Initialize global resources
60
- await create_httpx_client(app)
61
- async with checker: # Starts background health loop
55
+ async with Proxy(app):
62
56
  yield
63
- await close_httpx_client(app)
64
57
 
65
58
  app = FastAPI(lifespan=lifespan)
66
59
 
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}")
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")
71
68
 
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
69
  ```
77
70
 
71
+ ## Advanced Examples:
72
+
73
+ Check [example.py](example.py) for full examples, including
74
+ - **Websocket Proxy**
75
+ - **Socket.IO Proxy**
76
+
78
77
  ## Advanced Proxying
79
78
 
80
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