Repository navigation
Description
Spun up from #411
Currently, the behavior of passing None to the timeout parameter in the various request entrypoints is not consistent:
import httpx
url = "https://httpbin.org/delay/10"
# (1)
httpx.get(url, timeout=None) # Times out after 5s
# (2)
client = httpx.Client(timeout=None)
client.get(url) # Does not timeout, returns after 10s
# (3)
client = httpx.Client()
client.get(url, timeout=None) # Times out after 5sWe should make sure passing None always results in the behavior (2), i.e. no timeout should be applied, as opposed to (1) and (3) where the default timeout configuration is used.
The implementation suggested in #411 is to use a sentinel object:
# models.py
UNSET = object()
# api.py
def get(..., timeout: TimeoutTypes = UNSET, ...):
...We need to switch to using UNSET in all relevant places. For example (though these may not end up being relevant, as I have not solved this myself):
- Functions in
httpx/api.py. - Methods of
BaseClient,ClientandAsyncClientinhttpx/client.py. - Methods of dispatchers in
httpx/dispatch/, esp..send(). - Possibly other places that aren't on the top of my head at the time of writing this. :)
Next, we need to check for that sentinel instead of None when deciding whether to use the default timeout config.
Notable places affected by this switch are:
.send()methods in clients and dispatchers (only use the client's or dispatcher'sself.timeoutiftimeoutisUNSET, instead ofNone).- The
.connect(),.read()and.write()methods on the concurrency backend classes:
httpx/httpx/concurrency/asyncio.py
Lines 63 to 67 in 86a0eb0
| async def read( | |
| self, n: int, timeout: TimeoutConfig = None, flag: TimeoutFlag = None | |
| ) -> bytes: | |
| if timeout is None: | |
| timeout = self.timeout |
Activity
gdhameeja commentedon Oct 3, 2019
I'd like to work on this.
jcugat commentedon Oct 3, 2019
@florimondmanca I don't understand the reason to use the sentinel value. Wouldn't be the same to keep
DEFAULT_TIMEOUT_CONFIGas the defaulttimeoutvalue forhttpx.Client()and all the top-level methods likehttpx.get()? That way we would avoid having to checkif timeout is UNSET.florimondmanca commentedon Oct 4, 2019
@jcugat Good question, thanks, As it turns out, we already default
DEFAULT_TIMEOUT_CONFIGin the constructors forClient,AsyncClient,ConnectionPool,HTTPConnection, etc. I'll edit the issue description based on this.But there's a few design decisions about clients and dispatchers (which all expose a
.send()method) that hint at using a sentinel value:DEFAULT_TIMEOUT_CONFIGas a default in their constructors..send(). To do this, clients and dispatchers need to distinguish between "the user did not pass a value" and "the user explicitly doesn't want any timeout".Right now
.send()cannot make that distinction, but it can be achieved if we switch toUNSETas a default fortimeoutin.send()and its sub-methods (e.g..connect()onHTTPConnection).florimondmanca commentedon Oct 4, 2019
This issue is more subtle than I initially thought it would be and will require to go through various places of the code base. Still a nice first issue, but more challenging than usual, so I removed the label. :)
gdhameeja commentedon Oct 4, 2019
@florimondmanaca can you assign it to me please?
lovelydinosaur commentedon Oct 4, 2019
The only place we need to use an
UNSETvalue is on the client.request(...)methods.That's because we need to differentiate between two difference cases:
timeoutis not set (Defer to whatever is set on the Client instance)timeout=None(Don't timeout, regardless of whatever is set on the Client instance)We don't need it on the client value, and we don't need it on the top-level API methods.
thebigmunch commentedon Oct 4, 2019
Except the top-level API methods default to
Noneand pass that value along, making the top-level API default to no timeout. I don't think that's desired. And that doesn't help the fact that the dispatcher cannot currently distinguish between "the user did not pass a value" and "the user explicitly doesn't want any timeout", as Florimond said earlier. Doing this would leave the behavior inconsistent across the library just as now.Or am I just getting my mind in a twist this morning? Or is this with the decision that the defaults in those places would be
DEFAULT_TIMEOUT_CONFIG?jcugat commentedon Oct 4, 2019
Ok, so if I got it right, in summary we want:
Clientinstance: leave defaultDEFAULT_TIMEOUT_CONFIG.request(...)methods: change it to have theUNSETsentinelhttpx/api.py: change it to have defaultDEFAULT_TIMEOUT_CONFIG.send()methods in clients and dispatchers: change it to have theUNSETsentinelIs this correct?
lovelydinosaur commentedon Oct 7, 2019
@thebigmunch Ah yeah, not sure what I was thinking there. Easiest, most consistent then would be for the top-level ones to use the
UNSETsentinal too, then. (Having them beDEFAULT_TIMEOUT_CONFIGwould also work just fine, but I guess we may as well make them mirror their equivelent methods)lovelydinosaur commentedon Oct 7, 2019
So, @jcugat I'd probably suggest the last three in that list should all be "use
UNSET".I think we've got that right, then?
2 remaining items
thebigmunch commentedon Oct 20, 2019
@florimondmanca
1 == True. So,Trueis technically a valid input, just not in a way people would likely expect.timeout=Falsedoes not really read as what you'd expect for "no timeout" when compared totimeout=None.httpxhas to be beholden to requests' API or any other library, it should be considered thathttpxwould be changing the meaning oftimeout=Noneas compared to requests and, IIRC,aiohttp. Can't say I've had enough experience with other http client libraries to determine if that's true of them or not.I certainly didn't really like the way
UNSETlooked going throughout the library either. My only semi-reasonable suggestion for a builtin constant that would mean don't timeout would be ellipsis. The problem there is that most people don't actually know that exists. And any prior http client library usage would probably lead them to assume they should useNone.thebigmunch commentedon Oct 20, 2019
Another option could be to just set the default parameter values to
5, leavingtimeout=Noneto mean no timeouts.florimondmanca commentedon Oct 20, 2019
@thebigmunch Thanks for your comments.
Your first point makes me think — what if we switched from
NonetoTrueas the default, and disallowedNonealtogether? Then, we'd be able to raise a nice and clean exception to our users:I mean, if we decided to break backwards compatibility w.r.t. to timeouts (Requests applies no timeouts by default, but HTTPX does), it'd be nice to make it loud a clear in the code, right?
florimondmanca commentedon Oct 20, 2019
Applying
DEFAULT_TIMEOUT_CONFIGinternally whenever relevant still seems like a valid option, yes. @sethmlarson also suggested this in #493 (comment). Upside: we'd be able to keeptimeout=Noneas "no timeout". Downside: bunch of changes required to the code, but I guess we can live up to that. :)lovelydinosaur commentedon Oct 22, 2019
Seems like a very sensible suggestion, right? Not sure why it took us such a winding road to get there. 😌
thebigmunch commentedon Oct 22, 2019
Sometimes its difficult to see the wood for the trees ^_^
florimondmanca commentedon Oct 22, 2019
So, unless I'm really missing something here, I think the change to
timeout=5is not as trivial as we'd like it to be. Consider this example:I guess we're expecting
timeout=1e-2to propagate to the.request()call, right?Except…
.request()has its owntimeoutparameter, so defaulting it totimeout=5too means we have no way to tell if atimeoutwas passed, or if we should fallback toself.timeout.And this pattern is present in virtually the entire codebase: in
.send(),.connect(),.read(),.write(), etc.I understand the initial goal of having
timeoutparameters present everywhere is to be able to use small components of HTTPX independently — and I think Starlette has proved that this approach is very beneficial. But it seems to me having a well-defined timeout everywhere and allowing components to be used independently or as part of a wider system is surprisingly not easy to achieve.Again, am I just missing something really obvious? 😕
florimondmanca commentedon Oct 22, 2019
I think this wouldn't be a problem if we were using
**kwargs. We'd basically be able to use everywhere:But obviously
**kwargsgoes against our efforts of providing good editor and type checking support, so, yeah… At this point, I can't find of any other way to check for "this kwarg was not given" other than using a sentinal value. (Or it seems like we'd need to abandon per-call timeout config, e.g. onConnectionPooland other dispatchers — and then maybe evenClient, because how are we then supposed to pass the overiddentimeoutto the dispatcher? By setting itstimeoutattribute (provided it has one)? — which is obviously not acceptable.)florimondmanca commentedon Oct 22, 2019
If anyone is up for tackling this (and give the problem another POV), I'd be super happy. I feel like I'm going crazy over the issues that arise when actually trying to make this change, and don't have the resources to dig any deeper right now unfortunately.
thebigmunch commentedon Oct 22, 2019
I definitely mentioned it in regard to
Clientand its methods in your PR. I hadn't thought past that to see if it cropped up even deeper though.In the prior art of
requests, they quite specifically kepttimeoutout ofSessionbecause they wanted to keep a semantic separation between the session and the adapter layers (client vs dispatcher in httpx).Again, probably the solution
requestsused for the same problem.thebigmunch commentedon Oct 22, 2019
This would also go against the decision to remove unsupported parameters for some of the methods.
thebigmunch commentedon Oct 22, 2019
Yeah, I hadn't yet dove deep enough to see the full extent of the rabbit hole... until just now. Ouch.