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
55It preserves ** methods, headers, cookies, query parameters, and streaming bodies** , while supporting retries,
66timeout 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
3232pip install httpx
@@ -38,12 +38,12 @@ pip install httpx
3838# app.py
3939from lilya.apps import Lilya
4040from 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
5454app = 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(
8686You'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
149149You can proxy WS endpoints too:
150150
151151``` python
152- proxy = ReverseProxy (" http://chat-service.local" )
152+ proxy = Relay (" http://chat-service.local" )
153153app = 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
280280secret = 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
323323import pytest
324324from lilya import Lilya
325325from lilya.routing import Include
326- from lilya.contrib.proxy.reverse import ReverseProxy
326+ from lilya.contrib.proxy.relay import Relay
327+
327328
328329class 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
400403def upstream_app ():
401404 return DummyUpstream()
402405
406+
403407@pytest.fixture
404408def 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
421426async 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):
614619import logging
615620
616621logger = logging.getLogger(" proxy" )
617- proxy = ReverseProxy (" http://upstream" , logger = logger)
622+ proxy = Relay (" http://upstream" , logger = logger)
618623```
619624
620625Produces log lines like:
0 commit comments