fastapi-reverse-proxy 0.3.0__tar.gz → 0.3.1__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.3.0/src/fastapi_reverse_proxy.egg-info → fastapi_reverse_proxy-0.3.1}/PKG-INFO +50 -2
  2. {fastapi_reverse_proxy-0.3.0 → fastapi_reverse_proxy-0.3.1}/README.md +48 -0
  3. {fastapi_reverse_proxy-0.3.0 → fastapi_reverse_proxy-0.3.1}/pyproject.toml +2 -2
  4. {fastapi_reverse_proxy-0.3.0 → fastapi_reverse_proxy-0.3.1}/src/fastapi_reverse_proxy/proxy_pass.py +50 -20
  5. {fastapi_reverse_proxy-0.3.0 → fastapi_reverse_proxy-0.3.1/src/fastapi_reverse_proxy.egg-info}/PKG-INFO +50 -2
  6. {fastapi_reverse_proxy-0.3.0 → fastapi_reverse_proxy-0.3.1}/LICENSE +0 -0
  7. {fastapi_reverse_proxy-0.3.0 → fastapi_reverse_proxy-0.3.1}/setup.cfg +0 -0
  8. {fastapi_reverse_proxy-0.3.0 → fastapi_reverse_proxy-0.3.1}/src/fastapi_reverse_proxy/__init__.py +0 -0
  9. {fastapi_reverse_proxy-0.3.0 → fastapi_reverse_proxy-0.3.1}/src/fastapi_reverse_proxy/health_check.py +0 -0
  10. {fastapi_reverse_proxy-0.3.0 → fastapi_reverse_proxy-0.3.1}/src/fastapi_reverse_proxy/load_balance.py +0 -0
  11. {fastapi_reverse_proxy-0.3.0 → fastapi_reverse_proxy-0.3.1}/src/fastapi_reverse_proxy/proxy_httpx.py +0 -0
  12. {fastapi_reverse_proxy-0.3.0 → fastapi_reverse_proxy-0.3.1}/src/fastapi_reverse_proxy.egg-info/SOURCES.txt +0 -0
  13. {fastapi_reverse_proxy-0.3.0 → fastapi_reverse_proxy-0.3.1}/src/fastapi_reverse_proxy.egg-info/dependency_links.txt +0 -0
  14. {fastapi_reverse_proxy-0.3.0 → fastapi_reverse_proxy-0.3.1}/src/fastapi_reverse_proxy.egg-info/requires.txt +0 -0
  15. {fastapi_reverse_proxy-0.3.0 → fastapi_reverse_proxy-0.3.1}/src/fastapi_reverse_proxy.egg-info/top_level.txt +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-reverse-proxy
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: A robust, streaming-capable reverse proxy for FastAPI including WebSocket support.
5
5
  Author-email: Tomás <tomas@suricatingss.xyz>
6
+ License-Expression: MIT
6
7
  Classifier: Programming Language :: Python :: 3
7
- Classifier: License :: OSI Approved :: MIT License
8
8
  Classifier: Operating System :: OS Independent
9
9
  Classifier: Framework :: FastAPI
10
10
  Classifier: Topic :: Internet :: Proxy Servers
@@ -97,6 +97,7 @@ The `proxy_pass` function and `LoadBalancer.proxy_pass` provide deep customizati
97
97
  | `additional_headers` | `dict` | Append custom headers to the proxied request. |
98
98
  | `override_headers` | `dict` | Use these headers *instead* of original request headers. |
99
99
  | `forward_query` | `bool` | Whether to append the incoming query string (Default: `True`). |
100
+ | `override_host` | `str` | Override the outbound `Host` header sent to the target (useful for multi-host/virtual-hosting backends that key off the original requested host). |
100
101
 
101
102
  ## Monitoring & Configuration
102
103
 
@@ -133,3 +134,50 @@ The library implements "deferred negotiation" for WebSockets:
133
134
 
134
135
  - **Termination Safety**: Resource cleanup (closing `httpx` clients and sockets) is triggered even on task cancellation (`BaseException`).
135
136
  - **Introspection-Based Compatibility**: Uses `inspect.signature` to automatically detect version-specific parameters in the `websockets` library.
137
+ - **RFC 7230 Compliant Header Handling**: Hop-by-hop headers (`Connection`,
138
+ `Transfer-Encoding`, `TE`, `Trailers`, `Keep-Alive`, `Proxy-Authenticate`,
139
+ `Proxy-Authorization`) are stripped from both outbound requests and responses,
140
+ per spec. WebSocket handshake headers (`Sec-WebSocket-Key`, `Upgrade`, etc.)
141
+ from the client are never forwarded to the target, avoiding handshake collisions.
142
+
143
+
144
+
145
+ ## Running Behind a Reverse Proxy (Nginx/Apache)
146
+
147
+ By default, `proxy_pass` and `proxy_pass_websocket` forward the client's original
148
+ headers as-is — they do **not** set or rewrite `X-Real-IP`, `X-Forwarded-For`,
149
+ `X-Forwarded-Proto`, or `X-Forwarded-Host`. If this library sits behind Nginx or
150
+ Apache (the common setup), your upstream server is responsible for setting those
151
+ headers before the request reaches this proxy:
152
+
153
+ ```nginx
154
+ location / {
155
+ proxy_pass http://your-fastapi-app;
156
+ proxy_set_header X-Real-IP $remote_addr;
157
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
158
+ proxy_set_header X-Forwarded-Proto $scheme;
159
+ proxy_set_header Host $host;
160
+ }
161
+ ```
162
+
163
+ For WebSocket routes, Nginx also needs explicit upgrade handling:
164
+
165
+ ```nginx
166
+ map $http_upgrade $connection_upgrade {
167
+ default upgrade;
168
+ '' close;
169
+ }
170
+
171
+ location /ws/ {
172
+ proxy_pass http://your-fastapi-app;
173
+ proxy_http_version 1.1;
174
+ proxy_set_header Upgrade $http_upgrade;
175
+ proxy_set_header Connection $connection_upgrade;
176
+ proxy_set_header X-Real-IP $remote_addr;
177
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
178
+ proxy_set_header X-Forwarded-Proto $scheme;
179
+ }
180
+ ```
181
+
182
+ Without this configuration, `X-Forwarded-*` headers will be empty or missing by
183
+ the time they reach your application.
@@ -72,6 +72,7 @@ The `proxy_pass` function and `LoadBalancer.proxy_pass` provide deep customizati
72
72
  | `additional_headers` | `dict` | Append custom headers to the proxied request. |
73
73
  | `override_headers` | `dict` | Use these headers *instead* of original request headers. |
74
74
  | `forward_query` | `bool` | Whether to append the incoming query string (Default: `True`). |
75
+ | `override_host` | `str` | Override the outbound `Host` header sent to the target (useful for multi-host/virtual-hosting backends that key off the original requested host). |
75
76
 
76
77
  ## Monitoring & Configuration
77
78
 
@@ -108,3 +109,50 @@ The library implements "deferred negotiation" for WebSockets:
108
109
 
109
110
  - **Termination Safety**: Resource cleanup (closing `httpx` clients and sockets) is triggered even on task cancellation (`BaseException`).
110
111
  - **Introspection-Based Compatibility**: Uses `inspect.signature` to automatically detect version-specific parameters in the `websockets` library.
112
+ - **RFC 7230 Compliant Header Handling**: Hop-by-hop headers (`Connection`,
113
+ `Transfer-Encoding`, `TE`, `Trailers`, `Keep-Alive`, `Proxy-Authenticate`,
114
+ `Proxy-Authorization`) are stripped from both outbound requests and responses,
115
+ per spec. WebSocket handshake headers (`Sec-WebSocket-Key`, `Upgrade`, etc.)
116
+ from the client are never forwarded to the target, avoiding handshake collisions.
117
+
118
+
119
+
120
+ ## Running Behind a Reverse Proxy (Nginx/Apache)
121
+
122
+ By default, `proxy_pass` and `proxy_pass_websocket` forward the client's original
123
+ headers as-is — they do **not** set or rewrite `X-Real-IP`, `X-Forwarded-For`,
124
+ `X-Forwarded-Proto`, or `X-Forwarded-Host`. If this library sits behind Nginx or
125
+ Apache (the common setup), your upstream server is responsible for setting those
126
+ headers before the request reaches this proxy:
127
+
128
+ ```nginx
129
+ location / {
130
+ proxy_pass http://your-fastapi-app;
131
+ proxy_set_header X-Real-IP $remote_addr;
132
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
133
+ proxy_set_header X-Forwarded-Proto $scheme;
134
+ proxy_set_header Host $host;
135
+ }
136
+ ```
137
+
138
+ For WebSocket routes, Nginx also needs explicit upgrade handling:
139
+
140
+ ```nginx
141
+ map $http_upgrade $connection_upgrade {
142
+ default upgrade;
143
+ '' close;
144
+ }
145
+
146
+ location /ws/ {
147
+ proxy_pass http://your-fastapi-app;
148
+ proxy_http_version 1.1;
149
+ proxy_set_header Upgrade $http_upgrade;
150
+ proxy_set_header Connection $connection_upgrade;
151
+ proxy_set_header X-Real-IP $remote_addr;
152
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
153
+ proxy_set_header X-Forwarded-Proto $scheme;
154
+ }
155
+ ```
156
+
157
+ Without this configuration, `X-Forwarded-*` headers will be empty or missing by
158
+ the time they reach your application.
@@ -4,16 +4,16 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fastapi-reverse-proxy"
7
- version = "0.3.0"
7
+ version = "0.3.1"
8
8
  authors = [
9
9
  { name="Tomás", email="tomas@suricatingss.xyz" },
10
10
  ]
11
11
  description = "A robust, streaming-capable reverse proxy for FastAPI including WebSocket support."
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.8"
14
+ license = "MIT"
14
15
  classifiers = [
15
16
  "Programming Language :: Python :: 3",
16
- "License :: OSI Approved :: MIT License",
17
17
  "Operating System :: OS Independent",
18
18
  "Framework :: FastAPI",
19
19
  "Topic :: Internet :: Proxy Servers",
@@ -13,13 +13,20 @@ from urllib.parse import urlparse
13
13
 
14
14
  logger = logging.getLogger("fastapi_reverse_proxy")
15
15
 
16
- # Hop-by-hop headers that should typically not be forwarded by a proxy
17
- # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/TE
16
+ # RFC 7230 §6.1 — hop-by-hop headers, strip on every proxied HTTP request/response
18
17
  EXCLUDED_HEADERS = {
19
- "connection", "keep-alive", "proxy-authenticate",
18
+ "connection", "keep-alive", "proxy-authenticate",
20
19
  "proxy-authorization", "te", "trailers", "transfer-encoding", "upgrade"
21
20
  }
22
21
 
22
+ # WebSocket handshake headers — belong to ONE handshake (client<->you),
23
+ # must not be reused for the separate handshake you<->target
24
+ WS_HANDSHAKE_HEADERS = {
25
+ "connection", "upgrade", "host", "sec-websocket-key",
26
+ "sec-websocket-version", "sec-websocket-extensions",
27
+ "sec-websocket-protocol"
28
+ }
29
+
23
30
  def url_normalize_ws(url:str):
24
31
  u = urlparse(url)
25
32
  return url_normalize(url.replace(u.scheme, "http", 1)).replace("http", u.scheme, 1)
@@ -44,7 +51,8 @@ async def proxy_pass(
44
51
  additional_headers: Optional[dict] = None,
45
52
  override_headers: Optional[dict] = None,
46
53
  override_body: Optional[bytes] = None,
47
- method: Optional[str] = None
54
+ method: Optional[str] = None,
55
+ override_host: Optional[str] = None
48
56
  ):
49
57
  """
50
58
  Forwards incoming HTTP requests to the target service using streaming.
@@ -78,23 +86,32 @@ async def proxy_pass(
78
86
  headers = dict(request.headers)
79
87
  # Identify the client's real IP and forward it
80
88
  client_host = request.client.host if request.client else "unknown"
81
- headers["X-Real-IP"] = client_host
82
- if "X-Forwarded-For" in headers:
83
- headers["X-Forwarded-For"] = f"{headers['X-Forwarded-For']}, {client_host}"
84
- else:
85
- headers["X-Forwarded-For"] = client_host
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
86
94
 
87
- headers["X-Forwarded-Proto"] = request.url.scheme
88
- headers["X-Forwarded-Host"] = headers.get("host", request.url.netloc)
95
+ #headers["X-Forwarded-Proto"] = request.url.scheme
96
+ #headers["X-Forwarded-Host"] = headers.get("host", request.url.netloc)
89
97
 
90
98
  # Apply additional headers
91
99
  if additional_headers:
92
100
  headers.update(additional_headers)
93
101
 
94
- # Let httpx handle the host header and connection management
95
- headers.pop("host", None)
102
+ if override_host:
103
+ # use the supplied host header
104
+ headers['host'] = override_host
105
+ else:
106
+ # Let httpx handle the host header
107
+ headers.pop("host", None)
108
+
109
+ # Let httpx handle connection management
96
110
  headers.pop("connection", None)
97
111
 
112
+ # Remove hop-to-hop headers
113
+ headers = {k: v for k, v in headers.items() if k.lower() not in EXCLUDED_HEADERS}
114
+
98
115
  client = None
99
116
  try:
100
117
  client = await get_httpx_client(request)
@@ -140,7 +157,7 @@ async def proxy_pass(
140
157
  resp_headers[k] = v
141
158
 
142
159
  resp_headers["X-Accel-Buffering"] = "no"
143
- resp_headers["Cache-Control"] = "no-cache"
160
+ #resp_headers["Cache-Control"] = "no-cache"
144
161
 
145
162
  async def cleanup():
146
163
  await rp_resp.aclose()
@@ -205,12 +222,13 @@ async def proxy_pass_websocket(
205
222
  headers = dict(override_headers)
206
223
  else:
207
224
  client_host = websocket.client.host if websocket.client else "unknown"
208
- headers = {
209
- "X-Real-IP": client_host,
210
- "X-Forwarded-For": client_host,
211
- "X-Forwarded-Proto": websocket.url.scheme,
212
- "X-Forwarded-Host": websocket.headers.get("host", websocket.url.netloc)
213
- }
225
+ 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
+ #}
214
232
 
215
233
  if additional_headers:
216
234
  headers.update(additional_headers)
@@ -218,6 +236,9 @@ async def proxy_pass_websocket(
218
236
  # Use subprotocols from scope if not provided explicitly
219
237
  supported_subprotocols = subprotocols or websocket.scope.get("subprotocols")
220
238
 
239
+ # Strip hop-to-hop headers
240
+ headers = {k: v for k, v in headers.items() if k.lower() not in WS_HANDSHAKE_HEADERS}
241
+
221
242
  try:
222
243
  # Determine the correct header parameter name for this version of websockets
223
244
  # Modern (12.0+): additional_headers, Legacy: extra_headers
@@ -235,6 +256,15 @@ async def proxy_pass_websocket(
235
256
  await websocket.accept(subprotocol=target_ws.subprotocol)
236
257
  await _handle_ws_bidirectional(websocket, target_ws)
237
258
 
259
+ except websockets.exceptions.InvalidStatus as e:
260
+ status = e.response.status_code
261
+ logger.error(f"WebSocket handshake rejected by upstream: {status}")
262
+ try:
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
267
+
238
268
  except BaseException as e:
239
269
  if not isinstance(e, asyncio.CancelledError):
240
270
  # If the connection fails before accept(), we can raise a proper 502
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-reverse-proxy
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: A robust, streaming-capable reverse proxy for FastAPI including WebSocket support.
5
5
  Author-email: Tomás <tomas@suricatingss.xyz>
6
+ License-Expression: MIT
6
7
  Classifier: Programming Language :: Python :: 3
7
- Classifier: License :: OSI Approved :: MIT License
8
8
  Classifier: Operating System :: OS Independent
9
9
  Classifier: Framework :: FastAPI
10
10
  Classifier: Topic :: Internet :: Proxy Servers
@@ -97,6 +97,7 @@ The `proxy_pass` function and `LoadBalancer.proxy_pass` provide deep customizati
97
97
  | `additional_headers` | `dict` | Append custom headers to the proxied request. |
98
98
  | `override_headers` | `dict` | Use these headers *instead* of original request headers. |
99
99
  | `forward_query` | `bool` | Whether to append the incoming query string (Default: `True`). |
100
+ | `override_host` | `str` | Override the outbound `Host` header sent to the target (useful for multi-host/virtual-hosting backends that key off the original requested host). |
100
101
 
101
102
  ## Monitoring & Configuration
102
103
 
@@ -133,3 +134,50 @@ The library implements "deferred negotiation" for WebSockets:
133
134
 
134
135
  - **Termination Safety**: Resource cleanup (closing `httpx` clients and sockets) is triggered even on task cancellation (`BaseException`).
135
136
  - **Introspection-Based Compatibility**: Uses `inspect.signature` to automatically detect version-specific parameters in the `websockets` library.
137
+ - **RFC 7230 Compliant Header Handling**: Hop-by-hop headers (`Connection`,
138
+ `Transfer-Encoding`, `TE`, `Trailers`, `Keep-Alive`, `Proxy-Authenticate`,
139
+ `Proxy-Authorization`) are stripped from both outbound requests and responses,
140
+ per spec. WebSocket handshake headers (`Sec-WebSocket-Key`, `Upgrade`, etc.)
141
+ from the client are never forwarded to the target, avoiding handshake collisions.
142
+
143
+
144
+
145
+ ## Running Behind a Reverse Proxy (Nginx/Apache)
146
+
147
+ By default, `proxy_pass` and `proxy_pass_websocket` forward the client's original
148
+ headers as-is — they do **not** set or rewrite `X-Real-IP`, `X-Forwarded-For`,
149
+ `X-Forwarded-Proto`, or `X-Forwarded-Host`. If this library sits behind Nginx or
150
+ Apache (the common setup), your upstream server is responsible for setting those
151
+ headers before the request reaches this proxy:
152
+
153
+ ```nginx
154
+ location / {
155
+ proxy_pass http://your-fastapi-app;
156
+ proxy_set_header X-Real-IP $remote_addr;
157
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
158
+ proxy_set_header X-Forwarded-Proto $scheme;
159
+ proxy_set_header Host $host;
160
+ }
161
+ ```
162
+
163
+ For WebSocket routes, Nginx also needs explicit upgrade handling:
164
+
165
+ ```nginx
166
+ map $http_upgrade $connection_upgrade {
167
+ default upgrade;
168
+ '' close;
169
+ }
170
+
171
+ location /ws/ {
172
+ proxy_pass http://your-fastapi-app;
173
+ proxy_http_version 1.1;
174
+ proxy_set_header Upgrade $http_upgrade;
175
+ proxy_set_header Connection $connection_upgrade;
176
+ proxy_set_header X-Real-IP $remote_addr;
177
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
178
+ proxy_set_header X-Forwarded-Proto $scheme;
179
+ }
180
+ ```
181
+
182
+ Without this configuration, `X-Forwarded-*` headers will be empty or missing by
183
+ the time they reach your application.