absinthe-graphql / absinthe-socket

Core JavaScript support for Absinthe WS-based operations
MIT License
148 stars 75 forks source link

unable to connect to absinthe API from python ws client. #64

Closed lastmeta closed 2 years ago

lastmeta commented 2 years ago

I want to use graphql ws subscriptions in a project I'm working on. My python client can't connect to my phx graphql absinthe websocket api. I'm actually just followed the pragmatic studios tutorial to create a graphql server, and in that tutorial they then use apollo in react to make a JS client that connects to it. I want to use a python client instead, so I made a simple one:

import json 
from time import sleep
import threading 
import websocket

class ClientConnection(object):
    def __init__(self, timeout=5, url='ws://localhost:8000', payload=None):
        self.received = None
        self.sent = None
        self.thread = None
        self.timeout = timeout
        self.url = url
        self.payload = payload
        self.connect()

    def onMessage(self, ws, message):
        ''' send message to flask or correct actor '''
        self.received = message
        print(f'message:{message}')

    def onError(self, ws, error):
        ''' send message to flask to re-establish connection '''
        print(error)
        # exit thread

    def onClose(self, ws, close_status_code, close_msg):
        ''' send message to flask to re-establish connection '''
        print('### closed ###')
        # exit thread

    def onOpen(self, ws):
        print('Opened connection')
        self.send(self.payload)

    def send(self, message: str):
        self.sent = message
        self.ws.send(message)

    def connect(self):
        #websocket.enableTrace(True)
        self.ws = websocket.WebSocketApp(
            self.url,
            on_open=self.onOpen,
            on_message=self.onMessage,
            on_error=self.onError,
            on_close=self.onClose)
        self.thread = threading.Thread(target=self.ws.run_forever, daemon=True)
        self.thread.start()
        while not self.ws.sock.connected and self.timeout:
            sleep(1)
            self.timeout -= 1

def establishConnection():
    ''' establishes a connection to the satori server, returns connection object '''
    print(f'establishing a connection...')
    return ClientConnection(
        #url='ws://localhost:8000', # mock_server.py
        url='ws://localhost:4000/graphql', # absinthe server returns 400
    )

if __name__ == '__main__':
    connection = establishConnection()
    while True:
        connection.send(input('what should the client say to the server? '))

This client works fine when I have it connect to a little mock server I made:

import asyncio
import websockets

# create handler for each connection
async def handle_connections(websocket, path):
    while True: 
        data = await websocket.recv()
        print(f"Data recieved as: {data}")
        print(f"echoing...")
        reply = f"Data recieved as: {data}"
        await websocket.send(reply)

start_server = websockets.serve(handle_connections, "localhost", 8000)

asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()

But when I connect to the phx server it recognizes the connection, gives no error, but on the client side it says the server returned a 400.

There must be something really simple I'm missing, like a header or something, because what good is a graphql api if clients can't use it? Although, maybe it's a complex thing I'm missing, otherwise why would you have to use a special package to connect to Absinthe (Apollo)? Anyway, can you help me see what I'm missing?

PS in searching for a solution I found someone else who seemed to have a similar issue: https://github.com/oliyh/re-graph/issues/59 maybe that will give some insight into what I'm experiencing.

maartenvanvliet commented 2 years ago

Could you show some of the phoenix backend code? Right now you connect to /graphql. Is that where your socket is mounted? The phoenix socket supports two transports, so note that the websocket transport is by default under /socket/websocket/.

A second issue is that the Absinthe.Phoenix backend is built on top of Phoenix channels. There's a Python client for Phoenix channels. However, if you want to use this in conjunction with Absinthe.Phoenix you'll have to implement graphql subscription bookkeeping as well. That is, the same role absinthe-socket fulfills.

Imo it makes more sense to use https://hex.pm/packages/absinthe_graphql_ws. A graphql_ws backend written in Elixir that works in Phoenix. It doesn't use Phoenix channels as it doesn't need them. There's a Python client as well for graphql_ws.

lastmeta commented 2 years ago

Could you show some of the phoenix backend code?

I'm not sure what would be most useful, here's the router for graphiql

scope "/" do
    pipe_through(:api)

    forward("/api", Absinthe.Plug, schema: SatoriWeb.GraphQL.Schema)

    forward("/graphiql", Absinthe.Plug.GraphiQL,
      schema: SatoriWeb.GraphQL.Schema,
      socket: SatoriWeb.UserSocket
    )
end

the graphiQL website says the ws url is ws://localhost:4000/user_socket but when I attempt that from my python client I actually get a 404

here's the entire servercode https://github.com/lastmeta/Satori/tree/main/server/satori

arjan commented 2 years ago

Looking at the server code and the python example, I think in your case the URL you should provide is ws://localhost:4000/user_socket/websocket.

lastmeta commented 2 years ago

ws://localhost:4000/user_socket/websocket

of course, that takes care of it! thanks!