-
Notifications
You must be signed in to change notification settings - Fork 255
Refactor WebSocketContext::read_message_frame #515
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
Refactor WebSocketContext::read_message_frame #515
Conversation
| /// Try to decode one message frame. May return None. | ||
| fn read_message_frame(&mut self, stream: &mut impl Read) -> Result<Option<Message>> { | ||
| if let Some(frame) = self | ||
| let frame = match self |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice if this could be written as let Some(frame) = ... else { ... }; but that's not available until Rust 1.65 and the MSRV here is 1.63.
e0bd90e to
49f024a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with these ones:
- ✅ Early return from the first
match. It definitely looks better! - ✅ Replacing
if let Some(ref mut msg) = self.incomplete { .. } else { return .. }withself.incomplete.as_mut().ok_or(..). Definitely looks more elegant!
As for excessive branching: while I generally agree that it may make code less readable and that read_message_frame() would benefit from some refactoring, I'm not sure that the rest of the changes bring the desired simplification for the following reasons:
- The current handling of the frame's opcode is just under 70 LOC. I may be biased, but I find 70 LOC that contain a "flat representation" of all possible cases (including errors) quite useful. With the new changes, I get a bit more confused, because, e.g., the handling of the reserved code for the
Ctlopcode is done within thematch, but the handling of the reserved code for theDataopcode is done in a different place (inside theDataMessageType::try_from()conversion function) for no apparent reason. - I'm generally confused about the new
DataMessageType. It feels like it was only introduced to remove a singlematchcase inside theread_message_frame()and that it has no utility otherwise. TheTryFrom<Data> for DataMessageTypeessentially states that we can createDataMessageTypefromData, by e.g. doingData::Binary => Self::Initial(IncompleteMessageType::Binary), which feels logically incorrect, because we cannot state that the message is incomplete before checkingfinfirst (frame.header().is_final). Another problem that it introduces is that it changes the order of operations: sincetry_from()transforms reserved code into an error,DataMessageType::try_from(data)?would early return an error when the reserved code is used. However, the previous implementation would not do so if theself.incompleteisSome(..)(it would return a different error), because the match arm for_ if self.incomplete.is_some()would be checked before theOpData::Reserved(i).
49f024a to
20a8c8f
Compare
|
I've pulled out the uncontroversial changes into separate commits while leaving the total change the same. As for the rest of the feedback:
|
|
Funnily enough I tried writing a test against the current #[test]
fn reserved_data_frame_type_in_incomplete_message() {
let mut incoming = WriteMoc(Cursor::new(&[
0x01, 0x06, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, // "Hello"
0x06, // reserved frame code
0x00, 0x06, 0x20, 0x74, 0x68, 0x65, 0x72, 0x65, // (continuation) " there"
]));
let mut context = WebSocketContext::new(Role::Client, None);
let err = context.read(&mut incoming).unwrap_err();
assert!(
matches!(err, Error::Protocol(ProtocolError::UnknownDataFrameType(0x06))),
"err was {err:?}"
);
}An error is produced by tungstenite-rs/src/protocol/frame/frame.rs Lines 193 to 196 in 6520d8f
|
Thanks! If you prefer, you could create a separate PR with those commits, so that I can merge them right away.
I agree that having |
Eagerly check conditions for a data frame and encode the result explicitly in a custom enum type. This lets us combine code from two match arms and eliminate a panic! that was impossible to hit but required by the compiler.
20a8c8f to
5da8286
Compare
|
I've pushed a new final commit on the branch that takes a more focused approach to the refactoring. I've preserved the top-level match on the frame opcode and the precedence of checks for error conditions. As noted in the commit description, this does let us get rid of a |
|
Thanks! I've pondered over the changes in order to understand what exactly I do not like about the current ( I think the main thing I find suboptimal about the implementation in The only way to get rid of this complication (without some major re-thinking / refactoring) is to split the processing of the data frame into 2 stages. I believe that your latest change tries to achieve it: judging from the code, the first I attempted to formulate a suggestion based on your changes and what I believe might address the points you dislike, while considering my concerns and maintaining code readability.
What do you think? |
Closing the loop: https://github.com/signalapp/tungstenite-rs supports |
Reduce the amount of nested code by returning earlier on errors and from self-contained logic instead of branching with if/else and match expressions. Use an enum type to consolidate common code and avoid a panic! call.