Skip to content
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

Feature Request: Enable half-closed with CloseWrite #507

Open
fortuna opened this issue Dec 27, 2024 · 5 comments
Open

Feature Request: Enable half-closed with CloseWrite #507

fortuna opened this issue Dec 27, 2024 · 5 comments

Comments

@fortuna
Copy link

fortuna commented Dec 27, 2024

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.

@mafredri
Copy link
Member

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.

@fortuna
Copy link
Author

fortuna commented Dec 30, 2024

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
Copy link
Author

fortuna commented Dec 31, 2024

@mafredri does this implementation support unlimited messages?

@fortuna
Copy link
Author

fortuna commented Jan 2, 2025

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
Copy link
Author

fortuna commented Jan 6, 2025

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants