Skip to content

Commit 7341cbf

Browse files
authored
Rename ReverseProxy to Relay (#286)
* Rename ReverseProxy to Relay
1 parent 513eca7 commit 7341cbf

7 files changed

Lines changed: 49 additions & 44 deletions

File tree

docs/en/docs/contrib/proxy/reverse-proxy.md renamed to docs/en/docs/contrib/proxy/relay.md

Lines changed: 38 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# ReverseProxy
1+
# Relay
22

3-
A **mountable ASGI reverse proxy** for Lilya that forwards HTTP **and optionally WebSocket** traffic to an upstream service.
3+
A **mountable ASGI relay** for Lilya that forwards HTTP **and optionally WebSocket** traffic to an upstream service.
44

55
It preserves **methods, headers, cookies, query parameters, and streaming bodies**, while supporting retries,
66
timeout handling, header policies, and structured logging.
@@ -10,7 +10,7 @@ Typical use case: you have **two Lilya apps**:
1010
- **App 1** (Authentication) handles login, logout, refresh, etc.
1111
- **App 2** (Your main API) wants to expose `GET/POST /auth/...` publicly but **forward** to App 1 under the hood.
1212

13-
With `ReverseProxy`, you mount a single catch‑all route on App 2 (e.g., `/auth`) that **proxies everything** to App 1.
13+
With `Relay`, you mount a single catch‑all route on App 2 (e.g., `/auth`) that **proxies everything** to App 1.
1414

1515
## Why a Proxy?
1616

@@ -26,7 +26,7 @@ With `ReverseProxy`, you mount a single catch‑all route on App 2 (e.g., `/auth
2626

2727
### Before continuing
2828

29-
Lilya uses `httpx` to create the `ReverseProxy` object. This means, to work with it **you must**:
29+
Lilya uses `httpx` to create the `Relay` object. This means, to work with it **you must**:
3030

3131
```shell
3232
pip install httpx
@@ -38,12 +38,12 @@ pip install httpx
3838
# app.py
3939
from lilya.apps import Lilya
4040
from lilya.routing import Include
41-
from lilya.contrib.proxy.reverse import ReverseProxy
41+
from lilya.contrib.proxy.relay import Relay
4242

43-
proxy = ReverseProxy(
43+
proxy = Relay(
4444
target_base_url="http://auth-service:8000", # internal service base URL
45-
upstream_prefix="/", # map "/auth/<path>" -> "/<path>" upstream
46-
preserve_host=False, # set Host to auth-service
45+
upstream_prefix="/", # map "/auth/<path>" -> "/<path>" upstream
46+
preserve_host=False, # set Host to auth-service
4747
# Optional: drop the Domain attribute from Set-Cookie so it binds to current host
4848
rewrite_set_cookie_domain=lambda _original: "",
4949
max_retries=2,
@@ -53,10 +53,10 @@ proxy = ReverseProxy(
5353
# The Main Lilya application
5454
app = Lilya(
5555
routes=[
56-
Include("/auth", app=proxy), # Everything under /auth/** is proxied
56+
Include("/auth", app=proxy), # Everything under /auth/** is proxied
5757
],
58-
on_startup=[proxy.startup], # start shared HTTP client (pool)
59-
on_shutdown=[proxy.shutdown], # close it cleanly
58+
on_startup=[proxy.startup], # start shared HTTP client (pool)
59+
on_shutdown=[proxy.shutdown], # close it cleanly
6060
)
6161
```
6262

@@ -86,14 +86,14 @@ app = Lilya(
8686
You'll keep the proxy as part of your project's contrib code or ship it as `lilya.contrib.proxy`. The examples below assume a local module:
8787

8888
```python
89-
from lilya.contrib.proxy.reverse import ReverseProxy
89+
from lilya.contrib.proxy.relay import Relay
9090
```
9191
## API reference
9292

93-
### `ReverseProxy(...)`
93+
### `Relay(...)`
9494

9595
```python
96-
ReverseProxy(
96+
Relay(
9797
target_base_url: str,
9898
*,
9999
upstream_prefix: str = "/",
@@ -149,7 +149,7 @@ pip install websockets
149149
You can proxy WS endpoints too:
150150

151151
```python
152-
proxy = ReverseProxy("http://chat-service.local")
152+
proxy = Relay("http://chat-service.local")
153153
app = Lilya(routes=[Include("/ws", app=proxy)])
154154
```
155155

@@ -244,7 +244,7 @@ When the browser calls only **App 2** (the proxy), and App 2 server‑side calls
244244
### 1. Auth service under `/auth/**` with Domain drop
245245

246246
```python
247-
proxy = ReverseProxy(
247+
proxy = Relay(
248248
"http://auth-service:8000",
249249
upstream_prefix="/",
250250
rewrite_set_cookie_domain=lambda _: "", # drop Domain
@@ -261,7 +261,7 @@ app = Lilya(
261261
### 2. Proxy a versioned API under `/billing/**`: `/api/v1/**` upstream
262262

263263
```python
264-
billing = ReverseProxy(
264+
billing = Relay(
265265
"http://billing-service.internal",
266266
upstream_prefix="/api/v1",
267267
)
@@ -279,7 +279,7 @@ app = Lilya(
279279
```python
280280
secret = os.getenv("INTERNAL_SERVICE_TOKEN")
281281

282-
proxy = ReverseProxy(
282+
proxy = Relay(
283283
"http://internal:9000",
284284
extra_request_headers={"X-Internal-Auth": secret},
285285
drop_request_headers=["x-forwarded-for"], # example of additional drops
@@ -291,15 +291,15 @@ proxy = ReverseProxy(
291291
### 4. Preserve the client `Host` header (rare, but occasionally required)
292292

293293
```python
294-
proxy = ReverseProxy("http://upstream:8000", preserve_host=True)
294+
proxy = Relay("http://upstream:8000", preserve_host=True)
295295
```
296296

297297
**Why**: Some upstreams compute logic based on `Host`. Most setups prefer `preserve_host=False`.
298298

299299
### 5. Follow upstream redirects (opt‑in)
300300

301301
```python
302-
proxy = ReverseProxy(
302+
proxy = Relay(
303303
"http://legacy:8080",
304304
follow_redirects=True,
305305
)
@@ -323,17 +323,19 @@ import httpx
323323
import pytest
324324
from lilya import Lilya
325325
from lilya.routing import Include
326-
from lilya.contrib.proxy.reverse import ReverseProxy
326+
from lilya.contrib.proxy.relay import Relay
327+
327328

328329
class DummyUpstream:
329330
async def __call__(self, scope, receive, send):
330331
if scope["type"] != "http":
331-
await self._text(send, 404, "Not Found"); return
332+
await self._text(send, 404, "Not Found");
333+
return
332334

333335
method, path = scope["method"], scope["path"]
334336
qs = scope.get("query_string", b"").decode("latin-1")
335337

336-
headers = {k.decode("latin-1"): v.decode("latin-1") for k,v in scope["headers"]}
338+
headers = {k.decode("latin-1"): v.decode("latin-1") for k, v in scope["headers"]}
337339
body = b""
338340
while True:
339341
event = await receive()
@@ -370,7 +372,7 @@ class DummyUpstream:
370372
"refresh=zzz; Path=/; Secure; SameSite=None",
371373
]
372374
return await self._text(send, 200, "ok",
373-
extra=[(b"set-cookie", c.encode("latin-1")) for c in cookies])
375+
extra=[(b"set-cookie", c.encode("latin-1")) for c in cookies])
374376

375377
if path.endswith("/large"):
376378
await send({"type": "http.response.start", "status": 200,
@@ -387,23 +389,25 @@ class DummyUpstream:
387389
async def _text(self, send, status, text, extra=None):
388390
headers = [(b"content-type", b"text/plain; charset=utf-8")]
389391
if extra: headers.extend(extra)
390-
await send({"type":"http.response.start","status":status,"headers":headers})
391-
await send({"type":"http.response.body","body":text.encode("utf-8")})
392+
await send({"type": "http.response.start", "status": status, "headers": headers})
393+
await send({"type": "http.response.body", "body": text.encode("utf-8")})
392394

393395
async def _json(self, send, status, payload):
394396
data = json.dumps(payload).encode("utf-8")
395-
await send({"type":"http.response.start","status":status,
396-
"headers":[(b"content-type", b"application/json")]})
397-
await send({"type":"http.response.body","body":data})
397+
await send({"type": "http.response.start", "status": status,
398+
"headers": [(b"content-type", b"application/json")]})
399+
await send({"type": "http.response.body", "body": data})
400+
398401

399402
@pytest.fixture
400403
def upstream_app():
401404
return DummyUpstream()
402405

406+
403407
@pytest.fixture
404408
def proxy_and_app(upstream_app):
405409
upstream_transport = httpx.ASGITransport(app=upstream_app)
406-
proxy = ReverseProxy(
410+
proxy = Relay(
407411
"http://auth-service.local",
408412
upstream_prefix="/",
409413
preserve_host=False,
@@ -417,6 +421,7 @@ def proxy_and_app(upstream_app):
417421
)
418422
return proxy, app, upstream_app
419423

424+
420425
@pytest.fixture
421426
async def client(proxy_and_app):
422427
proxy, app, _ = proxy_and_app
@@ -563,13 +568,13 @@ async def test_ws_proxy(proxy_and_app):
563568

564569
async with websockets.serve(echo, "127.0.0.1", 0) as server:
565570
uri = f"ws://{server.sockets[0].getsockname()[0]}:{server.sockets[0].getsockname()[1]}"
566-
proxy = ReverseProxy(uri)
571+
proxy = Relay(uri)
567572
...
568573
```
569574

570575
## Troubleshooting
571576

572-
### ReverseProxy not started. Call startup() on app startup.
577+
### Relay not started. Call startup() on app startup.
573578

574579
- **Cause**: App lifespan didn't run (common when using `httpx.ASGITransport(app=app)`).
575580
- **Fix**:
@@ -614,7 +619,7 @@ async def test_ws_proxy(proxy_and_app):
614619
import logging
615620

616621
logger = logging.getLogger("proxy")
617-
proxy = ReverseProxy("http://upstream", logger=logger)
622+
proxy = Relay("http://upstream", logger=logger)
618623
```
619624

620625
Produces log lines like:

docs/en/docs/release-notes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ hide:
99

1010
### Added
1111

12-
- [ReverseProxy](./contrib/proxy/reverse-proxy.md). This allows to create objects that are ASGI compatible and upstream services within your Lilya application.
12+
- [Relay](./contrib/proxy/relay.md). This allows to create objects that are ASGI compatible and upstream services within your Lilya application.
1313
- **WebSocket proxying**: Added full support for bidirectional WS proxying (text + binary frames).
1414
- **Retry & backoff**: Configurable retry logic with exponential backoff on retryable statuses/exceptions.
1515
- **Timeout mapping**: Upstream timeouts now map to `504 Gateway Timeout`.

docs/en/mkdocs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ nav:
136136
- contrib/files/send-file.md
137137
- contrib/files/jsonify.md
138138
- Proxy:
139-
- contrib/proxy/reverse-proxy.md
139+
- contrib/proxy/relay.md
140140
- contrib/mail.md
141141
- contributing.md
142142
- sponsorship.md
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
raise ImportError("httpx is required for lilya.contrib.proxy") from e
1616

1717

18-
class ReverseProxy:
18+
class Relay:
1919
"""
20-
ASGI reverse proxy middleware for Lilya.
20+
ASGI relay middleware for Lilya.
2121
2222
This component forwards incoming ASGI requests to an upstream server
2323
using `httpx.AsyncClient` and streams the response back to the caller.
@@ -38,7 +38,7 @@ class ReverseProxy:
3838
Typical usage
3939
-------------
4040
```python
41-
proxy = ReverseProxy("http://upstream.local", upstream_prefix="/api")
41+
proxy = Relay("http://upstream.local", upstream_prefix="/api")
4242
4343
app = Lilya(routes=[Include("/proxy", app=proxy)])
4444
@@ -177,7 +177,7 @@ async def shutdown(self) -> None:
177177

178178
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
179179
"""
180-
ASGI entrypoint for the reverse proxy.
180+
ASGI entrypoint for the relay.
181181
182182
This method is invoked by the ASGI server whenever a new
183183
connection is routed to the proxy. It inspects the `scope["type"]`
@@ -199,7 +199,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
199199
Example:
200200
When mounted at `/auth`:
201201
202-
app = Lilya(routes=[Include("/auth", app=ReverseProxy(...))])
202+
app = Lilya(routes=[Include("/auth", app=Relay(...))])
203203
204204
The ASGI server will call `proxy(scope, receive, send)` whenever
205205
a request matches the `/auth` prefix.
@@ -213,7 +213,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
213213
else:
214214
await self._send_text(send, 404, "Not Found")
215215

216-
assert self._client is not None, "ReverseProxy not started. Call startup() first."
216+
assert self._client is not None, "Relay not started. Call startup() first."
217217

218218
async def _handle_http(self, scope: Scope, receive: Receive, send: Send) -> None:
219219
"""
@@ -239,7 +239,7 @@ async def _handle_http(self, scope: Scope, receive: Receive, send: Send) -> None
239239
Side effects:
240240
Emits structured logs for retries, timeouts, and errors.
241241
"""
242-
assert self._client is not None, "ReverseProxy not started. Call startup() first."
242+
assert self._client is not None, "Relay not started. Call startup() first."
243243

244244
method = scope["method"]
245245
upstream_url = self._build_upstream_url(scope)

tests/contrib/proxy/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import pytest
66

77
from lilya.apps import Lilya
8-
from lilya.contrib.proxy.reverse import ReverseProxy
8+
from lilya.contrib.proxy.relay import Relay
99
from lilya.routing import Include
1010

1111

@@ -144,7 +144,7 @@ def proxy_and_app(upstream_app):
144144
target_base = "http://auth-service.local"
145145

146146
upstream_transport = httpx.ASGITransport(app=upstream_app)
147-
proxy = ReverseProxy(
147+
proxy = Relay(
148148
target_base_url=target_base,
149149
upstream_prefix="/", # map /auth/<path> -> /<path> on upstream
150150
preserve_host=False,
File renamed without changes.

0 commit comments

Comments
 (0)