Fix missing join_ref in PushMessage for Phoenix V2 protocol - #84
Conversation
When pushing messages to a Phoenix channel, the V2 JSON serializer expects a join_ref to be included in the message array: [join_ref, ref, topic, event, payload] Without the join_ref, the server cannot properly route the message to the correct channel process, causing client->server messages to be silently dropped while server->client messages work fine. The fix adds join_ref from state.joins[topic], matching the pattern already used in the LeaveTopic handler.
|
It looks like #82 didn't touch this block so I'm not sure it's related. Do you have a reproduction case for the dropped messages? Ideally we can add a test case for this bug |
|
Maybe I am using the library incorrectly but here is a reproduction: https://github.com/evan-hines-js/slipstream_repro main branch uses my fork, no_fork branch uses the official release Run the docker compose for the postgres db and then run the server. On the main branch I see On the no_fork branch, all the messages are lost: |
|
With this reproduction repo, I went back through the Slipstream versions and none of them seem to work. Which is highly unusual because my projects just randomly stopped working without this fork even when I went back 10-20 commits (multiple days) in my own repo. Edit: Found it. Added a new branch to the repro repo no_fork_1.8.2. The issue only occurs with Phoenix 1.8.3 which, according to the changelog has a bug fix that affected how stale join_refs are handled. Now they are dropped. https://hexdocs.pm/phoenix/changelog.html So I'm not sure if this change is needed as-is or something that needs to be investigated further. For now I'll pin Phoenix or use my fork. |
|
Ah ok, that makes sense that it was a change in Phoenix instead. If I |
Because of phoenixframework/phoenix@c73bbfc, and that we don't control when devices update their Slipstream so that they work with this Phoenix change (CuatroElixir/slipstream#84), this commit fills in the `join_ref` when its missing.
Because of phoenixframework/phoenix@c73bbfc, and that we don't control when devices update their Slipstream so that they work with this Phoenix change (CuatroElixir/slipstream#84), this commit fills in the `join_ref` when its missing.
Because of phoenixframework/phoenix@c73bbfc, and that we don't control when devices update their Slipstream so that they work with this Phoenix change (CuatroElixir/slipstream#84), this commit fills in the `join_ref` when its missing.
|
This was a bug on the Phoenix side. It is fixed in 1.8.12. We do recommend to send the join ref on each message, so this is good! |
Bumps Phoenix to 1.8.12 and removes the `join_ref` workaround from `DeviceSocket`. ## Background Older Slipstream releases only send a `join_ref` on `phx_join`, leaving it `nil` on every message after that ([CuatroElixir/slipstream#84](CuatroElixir/slipstream#84)). Phoenix tightened its staleness check in [c73bbfc](phoenixframework/phoenix@c73bbfc) and started discarding those messages. Since we can't force deployed devices onto a newer Slipstream, `DeviceSocket` decoded each frame, patched the `join_ref` back in, and re-encoded it before handing it to the channel. [phoenixframework/phoenix#6800](phoenixframework/phoenix#6800) relaxed the check. The guard used to only tolerate a nil ref on the stored side: ```elixir # 1.8.11 - matches when the server's stored ref is nil (v1 protocol) %{^pid => {^topic, existing_join_ref}} when existing_join_ref in [join_ref, nil] -> ``` Slipstream joins *with* a `join_ref`, so the stored one is non-nil, and subsequent messages fell through to the "stale, ignore" clause. 1.8.12 tolerates nil on the incoming side, which is exactly our case: ```elixir # 1.8.12 %{^pid => {^topic, existing_join_ref}} when is_nil(join_ref) or join_ref === existing_join_ref -> ``` Stale non-nil refs are still dropped. The workaround never affected those anyway. It only ever filled in refs that were nil. ## Changes - `mix.lock`: phoenix 1.8.11 → 1.8.12, and bandit 1.12.4 → 1.12.5 pulled in transitively. No `mix.exs` change needed, `~> 1.8.4` already allowed it. - `DeviceSocket`: dropped `maybe_fix_join_ref/2`. The `handle_in/2` override stays because `heartbeat/1` still needs it. - Added a test covering a legacy client, plus the two client serializers behind it. ## On the test The suite pins Slipstream 1.2.2, the fixed client, so nothing in it was exercising the path the workaround existed for. Removing the code without a legacy client on the wire would have been unguarded. `SocketClient.LegacyJoinRefSerializer` and its msgpack counterpart send a `join_ref` on `phx_join` and `nil` on everything after, the way old Slipstream does. The new test joins over one of them, pushes `connection_types`, and asserts the device's connection metadata gets updated. I pinned back to 1.8.11 to check the test actually fails without the fix: - **1.8.11**: fails on both wire formats, metadata is `nil`, message silently dropped - **1.8.12**: passes on both ## A bug that went with it The removed code had a latent bug. It decoded with `socket.serializer` but re-encoded as JSON unconditionally, so a msgpack device sending a nil `join_ref` would have had JSON handed to the msgpack decoder. Probably unreachable in practice, since msgpack devices are on newer Slipstream, but it's gone now either way. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The join_ref is currently nil when receiving messages in a client -> server flow. Without the join_ref, the server cannot properly route the message to the correct channel process, causing client->server messages to be silently dropped while server->client messages work fine.
The fix adds join_ref from state.joins[topic], matching the pattern already used in the LeaveTopic handler.
This randomly started happening in several of my projects in the last week without explicit code changes so seems to have been added in 1.2.1 during the struct refactor although I have not investigated deeply.