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.
Files changed (15) hide show
  1. {fastapi_reverse_proxy-0.3.1 → fastapi_reverse_proxy-0.4.0}/PKG-INFO +23 -2
  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
  3. {fastapi_reverse_proxy-0.3.1 → fastapi_reverse_proxy-0.4.0}/pyproject.toml +5 -1
  4. {fastapi_reverse_proxy-0.3.1 → fastapi_reverse_proxy-0.4.0}/src/fastapi_reverse_proxy/proxy_pass.py +45 -49
  5. fastapi_reverse_proxy-0.3.1/README.md → fastapi_reverse_proxy-0.4.0/src/fastapi_reverse_proxy.egg-info/PKG-INFO +47 -1
  6. {fastapi_reverse_proxy-0.3.1 → fastapi_reverse_proxy-0.4.0}/LICENSE +0 -0
  7. {fastapi_reverse_proxy-0.3.1 → fastapi_reverse_proxy-0.4.0}/setup.cfg +0 -0
  8. {fastapi_reverse_proxy-0.3.1 → fastapi_reverse_proxy-0.4.0}/src/fastapi_reverse_proxy/__init__.py +0 -0
  9. {fastapi_reverse_proxy-0.3.1 → fastapi_reverse_proxy-0.4.0}/src/fastapi_reverse_proxy/health_check.py +0 -0
  10. {fastapi_reverse_proxy-0.3.1 → fastapi_reverse_proxy-0.4.0}/src/fastapi_reverse_proxy/load_balance.py +0 -0
  11. {fastapi_reverse_proxy-0.3.1 → fastapi_reverse_proxy-0.4.0}/src/fastapi_reverse_proxy/proxy_httpx.py +0 -0
  12. {fastapi_reverse_proxy-0.3.1 → fastapi_reverse_proxy-0.4.0}/src/fastapi_reverse_proxy.egg-info/SOURCES.txt +0 -0
  13. {fastapi_reverse_proxy-0.3.1 → fastapi_reverse_proxy-0.4.0}/src/fastapi_reverse_proxy.egg-info/dependency_links.txt +0 -0
  14. {fastapi_reverse_proxy-0.3.1 → fastapi_reverse_proxy-0.4.0}/src/fastapi_reverse_proxy.egg-info/requires.txt +0 -0
  15. {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.1
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` | Send custom data instead of the incoming request body. |
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` | Send custom data instead of the incoming request body. |
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.3.1"
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"]
@@ -1,10 +1,13 @@
1
- from fastapi import Request, WebSocket, Response, HTTPException
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
- content = override_body
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.aiter_bytes(),
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
- 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
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
- # If the connection fails before accept(), we can raise a proper 502
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
- # This is a bit tricky in WS, but if we haven't accepted yet, we can raise
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
- raise e
281
- finally:
282
- try:
283
- await websocket.close()
284
- except Exception:
285
- pass
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
- except (Exception, asyncio.CancelledError):
302
- # Exit loop on error or cancellation
303
- pass
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 (Exception, asyncio.CancelledError, websockets.ConnectionClosed):
314
- # Exit loop on error, closure, or cancellation
315
- pass
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` | Send custom data instead of the incoming request body. |
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.