-
Notifications
You must be signed in to change notification settings - Fork 215
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Exceptions from send
, and expected behaviors on closed connections.
#66
Comments
Prompted by encode/starlette#56 and encode/starlette#58 (comment) |
I totally agree that it's just going to happen, due to the async nature of the code in question, that you're going to send just after the socket is disconnected in any case - even if you have a perfect disconnect handling loop. I tell client code writers to write code that expects disconnects at any time, we should be thinking the same way. My questioning of the problem before was around safety - is it better for us to suppress those errors and maybe let bugs go uncaught? Initially I was erring on the side of not suppressing errors, which tends to be my default, but now I think I would agree that if we didn't suppress them, people would just see these errors randomly and they'd become ignored noise, which is perhaps even more dangerous. Given that, I'm more than happy to enshrine in the spec that "send should not raise errors if used after disconnect". The only exception I might carve out is if the server sent a disconnect packet and then sent something else, but that's more a nice thing that mature servers could maybe raise a warning over rather than an edge case I want to specify out. |
Oh absolutely yes. Sending invalid state transitions should raise errors.
Good point, yes. I don't think we necessarily need to strongarm "servers must not do this", more that "server implementations should prefer not to do this, and instead prefer to guard against poor app behavior with an enforced timeout after disconnect" (A server implementation could validly raise an error if tasks fail to terminate within the timeframe after a disconnect.) At some point I'll aim to have a little dig into daphne, hypercorn, and uvicorn's current behavior here, since I doubt it's in line with this proposal. (Paging @pgjones) |
OK, then I think we're agreed. I think this belongs in the |
I've reraised the same concern 5 years later on #405... 🤣 The proposed agreement here doesn't satisfy the case the user doesn't even call import asyncio
from websockets import exceptions as ex
from wsproto.utilities import LocalProtocolError
async def app(scope, receive, send):
await send({"type": "websocket.accept"})
try:
while True:
await send({"type": "websocket.send", "text": "potato"})
await asyncio.sleep(1)
except ex.WebSocketException:
print("Server exception leaked to application.")
except LocalProtocolError:
print("Server exception leaked to application.") I've proposed an alternative approach on #405 (comment), but I'm happy with any solution that doesn't leak the server exception to the application - I don't want the ASGI app to try to catch exceptions that are related to the server. |
My position remains the same as it was on #405 - in cases like this, there should be a single kind of exception that the server may raise if the send() call cannot be done for some reason, and we can collapse any internal server errors down to those. The key thing I want to preserve is that there is enough debugging information in that error (e.g. in its first message argument) so that a user can work out what's going on. |
You didn't comment about what I proposed (which seems aligned with what you said, right?). What are your thoughts about it? |
Oh, I missed that one in the flurry of comments. I don't think an extension for this makes sense; there's only so far we can push things like this. |
Well, it doesn't need to be an extension. I just think it makes sense for the web application to be ready to receive the exception, and then raise something more meaningful. |
What if it's a new field in the scope? |
I still don't see a particular advantage over just always saying it must subclass IOError - it's a builtin exception class that fits (you're doing IO) and is unlikely to be used by web applications at that point for anything else. |
Ok. It solves my needs. I can change the documentation to reflect this decision. Can I? Is this considered a breaking change or a bug fix in terms of the specification? |
Of course, please submit a PR and I'll review it. It's probably worthy of a minor version update, since it's not something that clients/servers can entirely ignore, but we're also not introducing something that will break things in new ways. |
Thanks. I'm a bit full in the next weeks, but I don't think there's much hurry here. I'll get back to it in November. :) Also, I never said this, but I really appreciate our interactions here @andrewgodwin 😁🙏 |
Yes, and I appreciate you helping me work through this! |
Let me try to summarize the discussion and see if I understood it correctly. We are going to make the server raise an error if A couple of thoughts:
|
I don't think I'm that concerned about backwards compat in this case - the behaviour here is already undefined depending on the server, and I think making that into an explicit error is an improvement. As for the Protocol - sure, but I'm not sure that's a blocker either way. |
@andrewgodwin We need to come back to this discussion. I don't think the |
An application could, in theory, still try to |
@andrewgodwin Are you disagreeing with your previous message: #66 (comment) ? I'm trying to understand this specific part:
|
To be fair, I wrote that previous message five years ago; in the time since, I have now come to believe that explicit errors in this case could be good, but if we make servers start raising them where they previously did not, that might be considered backwards incompatible, which is why I only want to raise them if the behaviour was undefined enough that not a lot of code was relying on this. That's a lot easier with, say, websocket code due to the much smaller surface area of who is writing it. Web handling code is trickier, since there's increasing numbers of ASGI-compatible web frameworks (which is great!). From the parts of those that I've seen, raising an error if you try to send after a HTTP disconnection might just be annoying (much like the "client already closed connection" errors you get from intermediary servers with this problem already), which is why I am floating the point that it totally can happen that you can |
I think that error would be annoying if it gets to the end user, but it doesn't necessarily need to. If it's part of the spec the ASGI application can catch the error and simply stop streaming the data / doing the work without raising an error. I do see the point that using the error as a mode of communication between the asgi server and application when the event is part of normal operation and doesn't require a user's intervention or attention is not a good use of exceptions I think the right thing to do would be for |
It would never get to the end user as, by its nature, they are disconnected at that point - it would just clutter up server error tracking (which it would then stop doing once people wrote apps to handle it, but you get my point).
Also, at the end of the day I want pragmatism rather than perfection, and I think returning an error on websocket send after closure is much more doable in a back-compat way (because it's something you should be expecting to handle anyway) - doing it on HTTP would be nice, but maybe something for a future spec version. |
right by "end users" I was referring to users of asgi frameworks. and as you say once applications handle it there won't be cluttering of server error logs. I don't think that would be a problem though: getting errors would require you to update your asgi server without updating your asgi web framework. People tend to need to update their web framework more often than their server (to get new features).
I'm not sure I'm understanding. I envisioned |
Well I am proposing that we instead say you can return a future that says if it's sent or not - servers can immediately set_result with true/false if they want to, or we then have the option where they can do it a little later if there's queues to flush. |
So you're proposing |
I'll take this part, and create a PR. |
@andrewgodwin we can make the HTTP version non-breaking if the server raises and catches the exception. That is, given any of these scenarios:
|
Ah yes, that is an interesting take on it - of course, it does bring us back to the problem of "which exception", since we can't have servers catch and squash anything except a unique exception, but we don't want to make them depend on asgiref... |
How about we make it a protocol of some sort? Say we add an |
It's always been my personal view that you can never assume that a send has happened even if the server thinks it has written bytes to a socket (as the client could disconnect while they're in transit, for example), and so disconnects aren't actually important as any sufficiently well-written protocol should be able to handle random disconnects at any point anyway, and not be saving important per-connection state; that's why I didn't add it to the protocol in the first place. People seem to want it, though, so it's those use-cases we have to consider in terms of what would work best and what wouldn't. I don't exactly know what the work that people would want to do after receiving a disconnection is? |
Fair points! Do you feel we need to look into what actions people are taking or want to take, or go with the assumption that since raising the exception gives you the same control as the disconnect message it's valuable as is? Not that we have to remove the disconnect message, we can have both. Given that we've figured out how to make it a non-breaking change, @Kludex maybe we can start by implementing it in Uvicorn and handling it in Starlette? |
I don't really know if we can get actually good user research on this, so I'm OK going with the backwards compatible solution if we can correctly specify what the exception should be. I'd suggest "a unique subclass of IOError specified by the server", so that user applications can catch IOError but the server can catch its own specific subclass. |
That sounds good to me! |
Some issues to lend to the importance of this: |
Looking at the linked issues, the spec is pretty clear that the server should wait for the The concerns with consuming the body (potentially unnecessarily, but then why send it?) would be better addressed by allowing the server to pass the request body as (say) a file descriptor, always in a single message, always with |
That's not clear to me, could you point out where it says that? And do you mean the framework should wait? It's the server that sets There's a trivial use case which users doing a hello world tutorial encounter: a streaming echo endpoint. Currently, that's impossible to write with Starlette because streaming a response listens for a disconnect (so that you stop streaming a response / doing work if the client disconnects) leading to a race condition in reading the request body. |
Sure. HTTP spec, receive event docs:
|
I'm not convinced that's the intention of that paragraph. IMO the wording is not super clear. And if that is what it means, isn't that very limiting? Are we saying that ASGI doesn't support chunk-by-chunk processing of a streaming request? |
Well... I think it is pretty clear. It says what it does. (But I don't want to argue that.) What (I think) we need is (to repeat from my earlier comment):
Having to consume the whole body, whether you want it or not isn't ideal, so give me a stream object I can consume on demand. I would prefer that, yes. Once the body is handled, any other message I get on This thread supposedly concerns |
As per the very long discussion in this thread it seems like the only backward compatible (as far as users are concerned) way to get disconnect information in
Agreed, I think where we disagree is that I believe that was the intention of the current spec /
This seems like a lot more code churn for everyone and I don't see how it'd be backward compatible. I'm not opposed to it but I'd like to see a concrete implementation or proposal. |
Yes, I've been following the thread for quite some time. My semi-idea is simply that the first I don't think orginalist arguments about intentions are that helpful. What we have is the wording of the spec. Removing |
Even better, we can know the intentions since I wrote most of the spec and I mostly remember my intentions. I do not agree with removing Now, I thought we were discussing the specific case of calling |
Ok no worries, I was proposing removing it in the next major version (which may never come) since it seems to become redundant. I'll remove that wording from the PR.
I guess it's worth clarifying for the record then: is the intention of the spec that you must buffer a request body in memory entirely before you begin to process it? |
I had hoped to avoid this being necessary, yes - this is why the |
Hey @andrewgodwin.
That's kind of the semi-idea that I sort of semi-want. ("Semi" here really meaning, I haven't looked into it in sufficient depth.) Could you perhaps outline what the issues are there? (I don't know them.) Thanks 🙏 (There's the whole multiple requests reading from the same input stream thing, leading to the need to ensure requests can only read their data, but I assume there'd be a way of avoiding that... 🤔) |
Well, the problem with that originally was that I wanted the entire body of asgi messages to be JSON-safe, as I had intended to do a process model where the process terminating the sockets was separate to the one running Django/etc. I think that's still a valuable property to try and maintain - it means you can prefork and have warm worker processes more easily, for example. |
We need to update the spec. It should be There's also |
Before digging into the meat of this topic I want to start with this assumption:
send
andreceive
should be treated as opaque, and should be allowed to bubble back up to the server in order to be logged. They may be intercepted by logging or 500 technical response middlewares, and subsequently re-raised, but applications shouldn't have any expectations about the type that may be raised, since these will differ from implementation to implementation.That's pretty much how the spec is currently worded, and I think that's all as it should be. An alternative design could choose to have exceptions be used in a limited capacity to communicate information back to the application, but I don't think that's likely to be desirable. An exception raised by the server in
send
orreceive
is a black box indicating an error condition with an associated traceback, and we shouldn't expect to catch or handle particular subclasses in differing ways.The core question in this issue is: 1. What expectations (if any) should the application have about the behavior when it makes a
send()
to a closed connection? 2. What behavior is most desirable from servers in that case?There are three possible cases for the application here:
In the case of HTTP, we've adopted "Note messages received by a server after the connection has been closed are not considered errors. In this case the send awaitable callable should act as a no-op." See #49
I don't recall where the conversation that lead to #49 is, but I think it's a reasonable behaviour because premature HTTP client disconnects once receiving the start of the response are valid behaviour, and not an error condition. If we raise an exception in that case then we end up with lots of erroneous exceptions being raised in response to perfectly valid server and client behavior.
If we're happy with that decision then there's two questions that it leads on to:
send
doesn't raise an exception once the connection has been closed?For the case of (1) I think that #49 means we have to have the expectation that the disconnect is communicated through the
receive
disconnect message. No exception should be raised from the server onsend
to the closed connection because that'd pollute server logs with false errors in the standard case, and we can't differentiate between long-polling connections and regular HTTP requests/responses.Mature server implementations probably would want to enforce a timeout limit on the task once the connection has been closed, and that is the guardrail against applications that fail to properly listen for and act on the disconnect.
For the case of (2) it's less obvious. We could perfectly well raise exceptions in response to
send
on a disconnected connection. (Which I assume is what all current implementations do.) The issue here is that we'd be raising logged error conditions on what is actually perfectly correct behavior... there's no guarantee that an application will have yet "seen" the disconnect message at the point it sends any given outgoing message.The upshot of all this is that I think the following would be the right sort of recommendations to make:
disconnect
is sent on the "receive" channel, and should forcibly kill the task if it has not completed after a reasonable timeout. Server implementations that do raise exceptions in response tosend
on a closed channel are valid, but may end up falsely logging server errors in response to standard client disconnects.Anyways, thorny issue, and more than happy to talk this through more, but I'm moderately sure that's the conclusion I'd come to.
The text was updated successfully, but these errors were encountered: