websocket icon indicating copy to clipboard operation
websocket copied to clipboard

Feature Request: Enable half-closed with CloseWrite

Open fortuna opened this issue 1 year ago • 6 comments

I'm writing a TCP reverse proxy over Websocket and I'm trying to use this library.

However, it provides no way to close the write side only. That's needed to indicate end of stream (EOF), which gets mapped to a FIN on the TCP connection to the origin.

Looking at the code, I see that Close() runs the full handhsake. Perhaps CloseWrite could just send the close message with the normal status, but not wait for the response. And the code should not close the connection when receiving the close message. It should only do that on the explicit Close() or CloseWrite() call by the application code.

fortuna avatar Dec 27 '24 17:12 fortuna

Hi, @fortuna.

Interesting use-case. From what you describe it sounds like it's somewhat in conflict with RFC 6455: 5.5.1:

If an endpoint receives a Close frame and did not previously send a Close frame, the endpoint MUST send a Close frame in response. (When sending a Close frame in response, the endpoint typically echos the status code it received.) It SHOULD do so as soon as practical.

Essentially this states that the close handshake should happen, i.e. we can't send off a close-frame without receiving one back. Although the receiving side could delay the close for the current message.

An endpoint MAY delay sending a Close frame until its current message is sent (for instance, if the majority of a fragmented message is already sent, an endpoint MAY send the remaining fragments before sending a Close frame). However, there is no guarantee that the endpoint that has already sent a Close frame will continue to process data.

But with regards to closing the underlying TCP connection, the spec is pretty clear:

After both sending and receiving a Close message, an endpoint considers the WebSocket connection closed and MUST close the underlying TCP connection. The server MUST close the underlying TCP connection immediately; the client SHOULD wait for the server to close the connection but MAY close the connection at any time after sending and receiving a Close message, e.g., if it has not received a TCP Close from the server in a reasonable time period.

In the clients case, we could probably change the behavior a bit, but I'm not sure if that would be helpful for your use-case? Given the above, does it sound like what you want to achieve is possible? If yes I'm probably not fully grokking what you're looking to do and would appreciate some more details.

mafredri avatar Dec 28 '24 15:12 mafredri

Unfortunately the RFC text is a bit limiting, but not completely.

For the "MUST send a Close frame in response", we will always send a Close, but later. The "as soon as practical" is a SHOULD, so we would not be in violation of that.

They say that the client MAY delay the Close until its current message is sent. But it doesn't say it MUST be sent right after the message, or that sending it after future messages is forbidden. That's where the RFC text is lacking. Sending it later is not in violation of the text, but the text doesn't make it clear it's safe, unfortunately.

As for closing the TCP connection, that's fine, because the client will send a TCP FIN when it sends the Websocket Close, and receive a FIN when it receives the Websocket Close, ensuring TCP is fully closed when Websocket is.

In my case, I control both the client and the server, so I could ensure that is working. However, that will certainly tie my implementation, and I'm not sure what other reverse proxies will do along the way. I intend to put my reverse proxy behind CDNs like Cloudflare, who may have a different interpretation of the RFC.

I'm still debating what's the best way to proceed:

  • Modify the library to support the half-close I mention in this issue
  • Implement an application-layer protocol on top of Websocket for TCP streams. That effectively introduces an new protocol, which I'd rather avoid.
  • Perhaps use boundless messages instead. I'm trying to figure out how that works. But if possible, I'll make the entire TCP stream be one message. That way it's clear the stream is done.

fortuna avatar Dec 30 '24 22:12 fortuna

@mafredri does this implementation support unlimited messages?

fortuna avatar Dec 31 '24 01:12 fortuna

So I tried the approach of using a single message for the entire stream, which gives me a FIN signal.

It almost worked, but it breaks due to the fact that the Writer.Write buffers the content, only flushing at the end of the message.

I need a mechanism to force the flush on every write, essentially removing the buffered writes.

fortuna avatar Jan 02 '25 20:01 fortuna

FYI, I was able to accomplish half-close with the Gorilla library. They allow us to provide a Close handler: https://pkg.go.dev/github.com/gorilla/websocket#Conn.SetCloseHandler. In my case, the close handler doesn't actually send a close back. I only do that on CloseWrite.

Though, to me it would still be preferable if I could send the entire stream as one message. I can accomplish that with gobwas via https://pkg.go.dev/github.com/gobwas/[email protected]/wsutil#Writer.FlushFragment or https://pkg.go.dev/github.com/gobwas/[email protected]/wsutil#Writer.WriteThrough.

Feature request:

  • Add SetCloseHandler
  • Add Writer.Flush or writer.WriteThrough

fortuna avatar Jan 06 '25 18:01 fortuna

@fortuna thanks for the investigation and status updates. Your feature request seems perfectly reasonable. 👍🏻

I haven't given a lot of thought yet as to how the custom Close handler fits in with the current API as there are multiple code paths that can close the connection. Would you expect the close handler to be the one called in all situations, e.g. when a timeout or read error occurs as well?

Also, just to clarify (since you also linked to FlushFragment), are you looking for FIN set to true or false when flushing?

mafredri avatar Jan 27 '25 11:01 mafredri