fix: splice a list-valued websocket reply into the command list - #400
Merged
Conversation
nova_websocket:call_result/0 documents that a controller may reply with
{reply, cow_ws:frame() | [cow_ws:frame()], State}, but the list form killed
the connection process.
nova_basic_handler:handle_ws/2 consed the payload onto `commands' as a
single element. nova_ws_handler passes that list to cowboy untouched and
cowboy treats every top-level element as one command, so a list-valued
reply arrived at cow_ws:frame/2 whole:
{function_clause,[{cow_ws,frame,[[{text,<<...>>},{text,<<...>>}],#{}],...
Ranch then logged the connection process exiting and the client's socket
dropped.
Splice the list in instead, preserving order - the accumulator is handed to
cowboy without a reverse, so its order is the order frames reach the wire.
No cow_ws:frame() is itself a list, so is_list/1 separates the two shapes
unambiguously, and an empty list contributes nothing.
Tests cover both handler levels: handle_ws/2 for the command list it
builds, and nova_ws_handler for what cowboy actually receives - including
that every element survives cow_ws:frame/2, which is the assertion that
would have caught this. The defect was invisible one level up, because the
controller's return value is correct in isolation.
Closes #399
Taure
added a commit
to widgrensit/asobi
that referenced
this pull request
Aug 4, 2026
…eleases Hex nova 0.15.1 crashes the connection process when a websocket controller replies with a list of frames, which nova_websocket:call_result/0 documents as valid: nova_basic_handler:handle_ws/2 consed the payload onto the command list as one element, cowboy reads one command per element, so the whole list reached cow_ws:frame/2 as a single frame and died with function_clause. That is why asobi_ws_handler emits exactly one frame per websocket_info/2 return, and why #330 put the producing extension in the payload rather than dual-emitting game.message and module.message as two frames. Fixed upstream in novaframework/nova#400, merged and tagged v0.15.3 but not yet on Hex. Pinned to the merge commit rather than {branch, "master"} so the ref cannot drift underneath a build. The delta from hex 0.15.1 is exactly two commits - that fix and a cowboy bump which changes nothing here, since cowboy/cowlib/ranch resolve to the same 2.18.0/2.19.0/2.2.1 either way. Verified through the real handler chain: a {reply, [F1, F2], State} now yields two commands, both encodable by cow_ws:frame/2. Revert to plain hex nova once it releases - asobi#347.
Taure
added a commit
to widgrensit/asobi
that referenced
this pull request
Aug 4, 2026
…eleases (#349) Hex nova 0.15.1 crashes the connection process when a websocket controller replies with a list of frames, which nova_websocket:call_result/0 documents as valid: nova_basic_handler:handle_ws/2 consed the payload onto the command list as one element, cowboy reads one command per element, so the whole list reached cow_ws:frame/2 as a single frame and died with function_clause. That is why asobi_ws_handler emits exactly one frame per websocket_info/2 return, and why #330 put the producing extension in the payload rather than dual-emitting game.message and module.message as two frames. Fixed upstream in novaframework/nova#400, merged and tagged v0.15.3 but not yet on Hex. Pinned to the merge commit rather than {branch, "master"} so the ref cannot drift underneath a build. The delta from hex 0.15.1 is exactly two commits - that fix and a cowboy bump which changes nothing here, since cowboy/cowlib/ranch resolve to the same 2.18.0/2.19.0/2.2.1 either way. Verified through the real handler chain: a {reply, [F1, F2], State} now yields two commands, both encodable by cow_ws:frame/2. Revert to plain hex nova once it releases - asobi#347.
Taure
added a commit
to widgrensit/asobi
that referenced
this pull request
Aug 4, 2026
…error (#354) * feat(ws): complete the S6 frame-type rename to module.message/module.error S6 put one extension (Lua) in the wire type, where no second extension could ever reuse it. #330 shipped the mechanism - the producing extension travels in the payload's `module` key - but not the rename, because nova 0.15.1 crashed the connection process on a list-valued reply, so dual-emitting old and new was impossible. novaframework/nova#400 fixed that and asobi carries the fixed nova by git ref (#349), so both frames can now go out on one reply. Extension pushes are `module.message` and `module.error`. `game.message` and `game.error` are emitted alongside them with identical payloads, so every SDK built before the rename keeps working, and are removed at the 1.0 wire break. `asobi.ws_legacy_game_frames` (default true) drops the legacy pair. `game.message` is asobi_lua's `game.send/2`, which a script may call per player per tick, so the compat frame doubles asobi's hottest extension-produced egress path. An operator whose clients all dispatch `module.*` gets that back without waiting for 1.0. Fixtures for both new types; the old two keep theirs. asobi_protocol_coverage_tests learns to read extension_frames/3, which is now the only emit site for either pair. * style: ASCII hyphen in the S6 test comment * docs: pin the wire-history note to v0.54.0
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #399.
nova_websocket:call_result/0documents{reply, OutFrame :: cow_ws:frame() | [OutFrame :: cow_ws:frame()], State :: map()}but the list form killed the connection process.
nova_basic_handler:handle_ws/2consed the payload ontocommandsas a single element.nova_ws_handler:handle_ws/4hands that list to cowboy untouched, and cowboy treats every top-level element as one command — so a list-valued reply reachedcow_ws:frame/2whole:Ranch logs the connection process exiting and the client's socket drops.
The fix
Order matters and is not reversed:
handle_ws/4returns the accumulator to cowboy as-is, so its order is the order frames reach the wire. Splicing in place is therefore correct andlists:reverse/1would be wrong.No
cow_ws:frame()is itself a list — they are atoms (close,ping,pong) and tuples — sois_list/1separates the two shapes unambiguously. An empty list contributes nothing.Tests
Both levels, because the defect is only visible at the lower one:
nova_basic_handler_tests— the command list built for a single frame, a list, an empty list, the hibernate variant, order preservation, earlier commands kept, and bare atom frames.nova_ws_handler_tests— what cowboy actually receives fromwebsocket_info/2andwebsocket_handle/2, plus an assertion that every element survivescow_ws:frame/2. That last one is what would have caught the original bug: the controller's return value is correct in isolation, so a test one level up passes while the connection dies.Reverting the source change fails 8 of them.
Checks
xref,dialyzer(no warnings in the changed files),eunit356/356,ct,ex_doc.elp lintreports one pre-existing L0002 onnova_basic_handler.erlthat is also present on master (an ELP internal crash, not a code defect).How it was found
An application emitted two frames from one
websocket_info/2clause. EUnit passed — the return value is right — and Common Test failed, because the defect only exists once Nova translates the return into cowboy commands.