oliyh / re-graph

A graphql client for clojurescript and clojure
460 stars 39 forks source link

difficulty connecting to Elixir/Phoenix/Absinthe server #59

Closed jmarca closed 4 years ago

jmarca commented 4 years ago

First of all, I don't really understand the ins and outs of web socket connections, so this bug report isn't very good.

My server side uses Elixir/Phoenix/Absinthe. (I can change that sure, but that means a lot of work)

I can use re-graph no problem to execute all graphql queries/mutations/etc over http.

The problem I'm having is setting up subscriptions, which needs a websocket link. I've tried a number of things, but nothing seems to work. Either I get a silent 400 error from the server, or else if I force a certain path, I can see a connection happening on my server, but with an error:

[debug] CONNECT IN USER SOCKET; connect_info are %{}; params are %{}
[info] CONNECTED TO RouteServerWeb.UserSocket in 203µs
  Transport: :websocket
  Serializer: Phoenix.Socket.V1.JSONSerializer
  Connect Info: %{}
  Parameters: %{}
[error] Ranch ... elixir error message ...exit with reason: {%Phoenix.Socket.InvalidMessageError{message: "missing key \"topic\""}

It is entirely possible that my server is not configured properly, but I am able to get things working with the graphiql client, so I think the server is okay. In graphiql advanced mode, I set the ws url to:

ws://172.18.0.5:4000/api

(although it actually seems to hit 172.18.0.5:4000/api/websocket?vsn=2.0.0)

and then I get this on the server:

[debug] CONNECT IN USER SOCKET; connect_info are %{}; params are %{"vsn" => "2.0.0"}
[info] CONNECTED TO RouteServerWeb.UserSocket in 212µs
  Transport: :websocket
  Serializer: Phoenix.Socket.V2.JSONSerializer
  Connect Info: %{}
  Parameters: %{"vsn" => "2.0.0"}
[info] JOINED __absinthe__:control in 54µs
  Parameters: %{}
[debug] ABSINTHE schema=RouteServerWeb.Schema variables=%{}
---
subscription {routes (veh: 1) {
  uid
  veh
  wkt
}}

The big difference seems to be that the graphiql client produced by Absinthe tacks on a channel topic __absinthe__:control How do I do that in re-graph on connection?

But I'm really grasping at straws here.

oliyh commented 4 years ago

Hi,

Do you have an instance of this server hosted anywhere so I can take a look? I'd like to try out the graphiql so I could see the websocket messages being sent. My hypothesis is that it is sending some form of handshake message in order to choose the "topic" (which is not in the graphql domain that I know of).

If not, would you be able to copy the output from Chrome devtools - open the Network tab, select the "WS" tab, then the websocket connection on the left, and then the "Frames" tab in the panel on the right. (You will need to open devtools before you make the websocket connection).

Also could you share the options that you are passing into re-graph? Have you tried setting :ws-url "172.18.0.5:4000/api/websocket?vsn=2.0.0"?

Thanks

jmarca commented 4 years ago

Thanks for the tips. I had indeed tried with and without vsn=2.0.0. I turned it back on.

Running in Chrome devtools (I'm one of those minority users who develops in firefox...), I see it is complaining about "Error during WebSocket handshake: Sent non-empty 'Sec-WebSocket-Protocol' header but no response was received"

Not sure how to disable that, nor do I know if it is worth it. The Absinthe devs closed a similar issue https://github.com/absinthe-graphql/absinthe/issues/661, and they point to using absinthe_phoenix js library, and the apollo wrapper. But I've been using that library in a separate fork of my client, and it has problems of its own, not to mention I'd have to redo all your excellent work in re-graph to get it to work easily with re-frame.

I'll keep poking, and I'll see if I can safely set up a public absinthe graphql server over the weekend.

(Beginning to think I should dump Phoenix and try clojure on the server side.)

Thanks

oliyh commented 4 years ago

Ah, this sounds related to #53 - you could try using the latest snapshot jar and setting :ws-sub-protocol nil in the options.

You should also be able to observe the headers being sent from Graphiql in the websocket request (same steps as above, but they're on the right under the 'request' tab instead of the 'frames' tab). If you see a value that is not "graphql-ws" then set the option to that.

jmarca commented 4 years ago

Hm. I think there is a bug. I'm going to make a pull request. When I fixed that, then it seems :ws-sub-protocol nil works.

I'm not seeing a "frames" tab in my version of chrome, but anyway, I am seeing differences between the standard phoenix code reload websocket and the one I'm trying to make for graphql.

The message for the phoenix code reload is:

(send) ["3","3","phoenix:live_reload","phx_join",{}]    45  
(recv) ["3","3","phoenix:live_reload","phx_reply",{"response":{},"status":"ok"}]
... then lots of heartbeat sends and receives ...

Note the payload is {}.

When using re-graph, I can't seem to be able to set the channel and event values.

(send) {"type":"connection_init","payload":{"test":"param","anothertest":"param"}}

(yeah, I'm just shoving random stuff into payload)

In short, the phoenix.js stock code seems to send a five-element array, with the last entry being the payload. With re-graph, I can only send the payload. Then the server sees that as %{"payload" => %{"anothertest" => "param", "test" => "param"}, "type" => "connection_init"}

I'll keep working on it as I have time.

oliyh commented 4 years ago

It sounds like the Phoenix implementation differs a lot from the Lacinia one. I'm not too surprised - the GraphQL spec says very little about the transport, especially websockets, and a de-facto standard based on what Apollo came up with seems to be the best we have.

What surprises me therefore is that graphiQL works for you - is it a stock standard one or is it a special Phoenix flavour?

jmarca commented 4 years ago

As to the graphiql, it is the modified Absinthe/Phoenix one.

And I have figured some things out, although I am not sure if a pull request is appropriate here or in the Absinthe/Phoenix side, or both.

In order to make things work I had to change how the payload is sent on connect in internals.cljc. Your version sends a hash (or struct or whatever...I can't recall what they're called in clojure) containing two elements-- a :type, and a :payload. I've changed that to just echo the passed-in connection-init-payload directly. I think it is reasonable to assume that the library user should be able to override this connection payload in order to play nicely with whatever quirks she finds in her websocket/graphql implementation.

(re-frame/reg-event-fx
 ::connection-init
 interceptors
  (fn [{:keys [db]} _]
    (let [ws (get-in db [:websocket :connection])
          payload (get-in db [:websocket :connection-init-payload])]
      (when payload
        ;; just send the connection init payload as
        {::send-ws [ws payload]}
        ;; {::send-ws [ws {:type "connection_init"
        ;;                 :payload payload}]}
        ))))

Second, in order to connect with Absinthe/Phoenix web socket, I had to send this as the connection init payload:

(defn my-default-ws-url []
  (when (exists? (.-location js/window))
    (let [host-and-port (.-host js/window.location)
          ssl? (re-find #"^https" (.-origin js/window.location))]
      (str (if ssl? "wss" "ws") "://" host-and-port "/api/websocket?vsn=2.0.0")))
  )
...
(re-frame/dispatch-sync
  [::re-graph/init
     {:ws-url  (my-default-ws-url)
     :ws-sub-protocol nil
     :http-url "/api"   ;; override the http url (defaults to /graphql)
     :connection-init-payload [1 1 "__absinthe__:control" "phx_join" {}]
     }])

That (ridiculous) array payload is from the phoenix.js implementation, in which there is this code:

export let Serializer = {
  encode(msg, callback){
    let payload = [
      msg.join_ref, msg.ref, msg.topic, msg.event, msg.payload
    ]
    return callback(JSON.stringify(payload))
  },
  decode(rawPayload, callback){
    let [join_ref, ref, topic, event, payload] = JSON.parse(rawPayload)
    return callback({join_ref, ref, topic, event, payload})
  }

I have no idea why Phoenix expects an array and returns an array instead of a JSON object, but that's how the sausage is made in this case.

With these edits, and with the previous pull request I referenced above applied, I can successfully connect a websocket to Phoenix/Absinthe. I haven't yet tried to use that connection, but I'm pretty stoked about this baby step.

jmarca commented 4 years ago

It seems the array format is somehow a standard for Phoenix sockets. Same thing is happening in the dart client here. So I guess I'll need to write a serializer/deserializer for cljs. Is there a good place to slot such a thing into this library as a callback?

oliyh commented 4 years ago

So does that array format apply to every message on the websocket, or just the connection init payload? This would have to be passed in as an optional parameter to re-graph at initialisation time, defaulting to the current behaviour, and it would replace these functions: https://github.com/oliyh/re-graph/blob/master/src/re_graph/internals.cljc#L24-L32

(but only for the websocket - encode is also used by http)

jmarca commented 4 years ago

Apparently it depends on the serializer used on the server, but for the default one, it needs an array every time

However, I've been asking on the Elixir forum and apparently one needs a more robust adapter that a vanilla websocket to connect to Phoenix channels. So I think I’m going to close this and try to write a proper adapter.

Call it re-gra-phx maybe...

James

lastmeta commented 1 year ago

any progress on this, I ran into the same issue trying to connect a python WS client to the phx graphql absinthe server.

jmarca commented 1 year ago

I honestly do not recall. I'll have to dig around, but I think I got it to work. Either that, or I changed backends.

Don't hold your breath waiting for me though because that project was on a laptop that I no longer use regularly, so it will take a bit of time/effort to dig it out.

Sorry I can't be of more help.

lastmeta commented 1 year ago

I found https://pypi.org/project/phxsocket/ so maybe that solves for the issue?

I'm surprised the exact specifications for how to connect on websocket from any kind of client are difficult to determine since absinthe is a popular package in the phx ecosystem. Maybe I just don't know where to look.

jmarca commented 1 year ago

Yeah me too. I think it’s just that specs are more difficult than implementations.

James

On Sep 7, 2022, at 16:28, Jordan Miller @.***> wrote:

 I found https://pypi.org/project/phxsocket/ so maybe that solves for the issue?

I'm surprised the exact specifications for how to connect on websocket from any kind of client are difficult to determine since absinthe is a popular package in the phx ecosystem. Maybe I just don't know where to look.

— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you modified the open/close state.