Skip to content

Commit 96162d7

Browse files
committed
Default to no timeout, add timeout= option, closes #10
Also improves returned errors, refs #6
1 parent 1c16fc9 commit 96162d7

File tree

3 files changed

+43
-9
lines changed

3 files changed

+43
-9
lines changed

README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ Now `app` is an ASGI application that will proxy all incoming HTTP requests to t
2727

2828
The function takes an optional second argument, `log=` - set this to a Python logger, or any object that has `.info(msg)` and `.error(msg)` methods, and the proxy will log information about each request it proxies.
2929

30+
It also takes a `timeout=` option, which defaults to `None` for no timeout. This can be set to a floating point value in seconds to enforce a timeout on requests that are being proxied.
31+
3032
## CLI tool
3133

3234
You can try this module out like so:
@@ -38,7 +40,7 @@ You may need to `pip install uvicorn` first for this to work.
3840

3941
This will start a server on port 8000 that proxies to `https://datasette.io`.
4042

41-
Add `-p PORT` to specify a different port, `--verbose` to see debug logging, and `--host 127.0.0.1` to listen on a different host (the default is `0.0.0.0`).
43+
Add `-p PORT` to specify a different port, `--timeout 3` to set a timeout, `--verbose` to see debug logging, and `--host 127.0.0.1` to listen on a different host (the default is `0.0.0.0`).
4244

4345
## Development
4446

asgi_proxy/__init__.py

+35-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
from httpx import AsyncClient
1+
import httpx
22
from urllib.parse import urlparse
33

44

5-
def asgi_proxy(backend, log=None):
5+
def asgi_proxy(backend, log=None, timeout=None):
66
backend_host = urlparse(backend).netloc
77

88
async def asgi_proxy(scope, receive, send):
@@ -32,7 +32,7 @@ async def asgi_proxy(scope, receive, send):
3232
body += message.get("body", b"")
3333
more_body = message.get("more_body", False)
3434

35-
async with AsyncClient() as client:
35+
async with httpx.AsyncClient(timeout=timeout) as client:
3636
try:
3737
# Stream it, in case of long streaming responses
3838
async with client.stream(
@@ -74,10 +74,39 @@ async def asgi_proxy(scope, receive, send):
7474
return
7575

7676
await send({"type": "http.response.body", "more_body": False})
77-
except Exception as e:
77+
except httpx.TimeoutException as ex:
78+
if log:
79+
log.error(f"Timeout error occurred: {ex.__class__.__name__}: {ex}")
80+
await send(
81+
{
82+
"type": "http.response.start",
83+
"status": 504,
84+
}
85+
)
86+
await send(
87+
{
88+
"type": "http.response.body",
89+
"body": b"Gateway timeout",
90+
"more_body": False,
91+
}
92+
)
93+
except Exception as ex:
7894
# Handle any errors during the request
7995
if log:
80-
log.error(f"An error occurred: {e.__class__.__name__}: {e}")
81-
return
96+
log.error(f"An error occurred: {ex.__class__.__name__}: {ex}")
97+
await send(
98+
{
99+
"type": "http.response.start",
100+
# Generic gateway error
101+
"status": 502,
102+
}
103+
)
104+
await send(
105+
{
106+
"type": "http.response.body",
107+
"body": b"Bad gateway",
108+
"more_body": False,
109+
}
110+
)
82111

83112
return asgi_proxy

asgi_proxy/__main__.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
parser.add_argument(
1919
"--host", default="0.0.0.0", help="Host to bind to (default: 0.0.0.0)"
2020
)
21+
parser.add_argument(
22+
"--timeout", type=float, default=None, help="Timeout in seconds"
23+
)
2124
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose logging")
2225

2326
args = parser.parse_args()
@@ -26,8 +29,8 @@
2629
import logging
2730

2831
logging.basicConfig(level=logging.INFO)
29-
app = asgi_proxy(args.url, log=logging)
32+
app = asgi_proxy(args.url, log=logging, timeout=args.timeout)
3033
else:
31-
app = asgi_proxy(args.url)
34+
app = asgi_proxy(args.url, timeout=args.timeout)
3235

3336
uvicorn.run(app, host=args.host, port=args.port)

0 commit comments

Comments
 (0)