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

WebSocket#close accepts code and reason arguments #7483

Closed
wants to merge 3 commits into from

Conversation

vladfaust
Copy link
Contributor

@vladfaust vladfaust commented Feb 26, 2019

Fixes #6469

I saw WebSocket#close specs and they are pretty simple, but I'm absolutely not sure how to convert String.new(bytes[2, bytes[1]]).should eq(message) line to test both reason and code arguments.

Also see https://developer.mozilla.org/ru/docs/Web/API/WebSocket/close.

@vladfaust
Copy link
Contributor Author

I'm also thinking of parsing on socket close frame as well:

message = @current_message.to_s

So a developer would write

socket.on_close do |reason, code|
  puts "Closed websocket with #{code} #{reason}"
end

src/http/web_socket.cr Outdated Show resolved Hide resolved
@z64
Copy link
Contributor

z64 commented Feb 26, 2019

Yes, we need to also have close frame decoding, but similar to my last comment, please:

ws.on_close do |code, reason|
  # ..
end

@vladfaust
Copy link
Contributor Author

Gonna work on it tomorrow

@vladfaust
Copy link
Contributor Author

Please help me writing socket.on_close { |code, reason| } specs (or write them for me, which is better)

@z64
Copy link
Contributor

z64 commented Feb 27, 2019

Hi @vladfaust, following the http negotiation spec I believe this should be acceptable. This passes for the existing 0.27.2 implementation:

it "calls close handler with code and reason" do
  address_chan = Channel(Socket::IPAddress).new

  spawn do
    http_ref = nil
    ws_handler = HTTP::WebSocketHandler.new do |ws, ctx|
      ws.on_message do
        # ws.close(1000, "goodbye")
        ws.close("goodbye")
        http_ref.not_nil!.close
      end
      ws.send("hello")
    end

    http_server = http_ref = HTTP::Server.new([ws_handler])
    address = http_server.bind_unused_port
    address_chan.send(address)
    http_server.listen
  end

  listen_address = address_chan.receive
  ws = HTTP::WebSocket.new("ws://#{listen_address}")

  ws.on_message do
    ws.send("hello")
  end

  # ws.on_close do |code, reason|
  ws.on_close do |message|
    # code.should eq 1000
    # reason.should eq "goodbye"
    message.should eq "goodbye"
  end

  ws.run
end

Edits for your new spec as commented.

Seeing as there aren't individual specs like this for the other handlers, maybe it should just be added to the spec I linked. I'm not sure though.

I would add whatever you think is best so that we can be sure it works, and then spec style can be addressed in review 👍

Copy link
Contributor

@z64 z64 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few other details, the rest I think is okay. I'll leave the rest to the core team, thank you!

send(Bytes.empty, Opcode::CLOSE)
end
def close
send(Bytes.empty, Opcode::CLOSE)
Copy link
Contributor

@z64 z64 Feb 27, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just my opinion, but I think def close should send 1000 Normal Closure (delegate close(1000)) per RFC 6455 Sec. 7.4.1

Please document this detail if you accept it

Edit: On second thought, maybe disregard. There is the reserved 1005 close code which is not explicitly sent, but is implicitly received when a client sends a close frame with no body. 🤷‍♂️

end

# Closes the websocket with *code* and *reason*.
def close(code : Int16, reason : String? = nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, shouldn't code be UInt16 everywhere?

a 2-byte unsigned integer (in network byte order) representing a status code with value /code/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Sija see in the discussion above

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't find it, which discussion?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/http/web_socket.cr Show resolved Hide resolved
src/http/web_socket.cr Show resolved Hide resolved
reason = @current_message.gets_to_end
@on_close.try &.call(code, reason)
close(code, reason) unless closed?
rescue IO::EOFError
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be ensure?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🏓

send(Bytes.empty, Opcode::CLOSE)
end

def close(code : Int16, reason : String? = nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

@vladfaust vladfaust changed the title WebSocket#close accepts reason and code arguments WebSocket#close accepts code and reason arguments Feb 27, 2019
@asterite
Copy link
Member

Can't code have a default value other than nil?

@z64
Copy link
Contributor

z64 commented Feb 27, 2019

@asterite In my suggestions I considered close code 1005, which is a special case; it is never meant to be sent by clients, but receiving clients interpret a body-less close frame as close 1005.

I suppose we could do

def close(code : UInt16 = 1005, reason : String? = nil)
  if code == 1005
    send(Bytes.empty, Opcode::CLOSE)
  else
    # send body
  end
end

but then this is confusing (to someone unfamiliar with ws protocol), because reason will never be sent:

ws.close(1005, "reason")
# or
ws.close(reason: "reason")

I think two methods is a cleaner separation of these semantics.

@bew
Copy link
Contributor

bew commented Mar 13, 2019

Reading https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Properties
I think that documented error codes should be available in an enum (with their reason), and a custom close with code+reason should be available for other lib/framework/application errors

@Sija
Copy link
Contributor

Sija commented Mar 13, 2019

@bew IMO using an enum here would be too limiting. Docs especially state that some codes are:

Reserved for use by WebSocket extensions.

or

Reserved for future use by the WebSocket standard.

@z64
Copy link
Contributor

z64 commented Mar 13, 2019

I don't think an enum would be bad.

Enums are not strict in accepting integer values by default, so I don't see how it would be limiting. reason though is just a payload, and is application specific. The name of the code is standard, though, much like HTTP status codes.

Basically, this proposal is #7247 but for websockets (see the enum HTTP::Status). I think this could be done in another PR though.

@vladfaust
Copy link
Contributor Author

FYI: I'm not very pushy about that because developing Crystal itself is hard on my machine I have this working in my own code. Should I add server specs here? 🤔

RX14 added a commit to RX14/crystal that referenced this pull request Mar 30, 2020
The first two bytes of the message in a WebSocket Close frame is a
numeric code. Make sure to always encode this correctly, to avoid being
in violation of the websocket spec. Also automatically decode this for
clients. This is a breaking change since on_close now takes two
arguments: close code and message, but this change was unable to be made
in a backwards compatible way.

Closes crystal-lang#6469
Closes crystal-lang#7483
@RX14 RX14 closed this in 46723ce Apr 1, 2020
carlhoerberg pushed a commit to carlhoerberg/crystal that referenced this pull request Apr 29, 2020
The first two bytes of the message in a WebSocket Close frame is a
numeric code. Make sure to always encode this correctly, to avoid being
in violation of the websocket spec. Also automatically decode this for
clients. This is a breaking change since on_close now takes two
arguments: close code and message, but this change was unable to be made
in a backwards compatible way.

Closes crystal-lang#6469
Closes crystal-lang#7483
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

WebSocket close frame messages aren't being parsed
6 participants