Here are some notes on WebSockets-based Absinthe GraphQL subscriptions. A basic setup and familiarity with regular queries and mutations is assumed.
Before we write our first subscription we need to ensure that the Absinthe.Subscription
process will be started:
# lib/app/application.ex
...
# Start the endpoint when the application starts
AppWeb.Endpoint,
# GraphQL subscriptions
{Absinthe.Subscription, [AppWeb.Endpoint]},
...
Next we have to update our Phoenix application endpoint to have the new WebSocket endpoint in place. Most people would want to start with one endpoint, use WebSockets, and handle the connection per user, hence pointing to AppWeb.UserSocket
module for authentication.
# lib/app_web/endpoint.ex
defmodule AppWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :app
use Absinthe.Phoenix.Endpoint
socket "/socket", AppWeb.UserSocket,
websocket: true,
longpoll: false
...
AppWeb.UserSocket
module needs to be defined of course. Here is an example of authenticating this socket connection using Guardian and OAuth 2 bearer token. We either return {:ok, socket}
tuple for successful connection or :error
atom to refuse the request. Also notice the Absinthe.Phoenix.Socket.put_options
function call to add the user identifier (we will use this soon in the subscription):
# lib/app_web/channels/user_socket.ex
defmodule AppWeb.UserSocket do
use Phoenix.Socket
use Absinthe.Phoenix.Socket,
schema: DigiWeb.Private.Schema
def connect(%{"Authorization" => header_content}, socket) do
[[_, token]] = Regex.scan(~r/^Bearer (.*)/, header_content)
case Guardian.Phoenix.Socket.authenticate(socket, DigiWeb.Public.Guardian, token) do
{:ok, authed_socket} ->
user_id = authed_socket.assigns.guardian_default_claims["sub"]
new_socket =
Absinthe.Phoenix.Socket.put_options(authed_socket,
context: %{
user_id: user_id
}
)
{:ok, new_socket}
{:error, _} ->
:error
end
end
# This function will be called when there was no authentication information
def connect(_params, _socket) do
:error
end
def id(_socket), do: nil
end
Once the connection itself is ready, we can update our GraphQL schema with a new subscription. I am including two similar yet different examples of a subscription for a file download. :user_download_ready
uses the socket context to publish to a “user_downloads:USER_ID” topic and :download_ready
uses a :download_id
identifier instead to publish to a “downloads:DOWNLOAD_ID” topic (this would require the download request mutation to return such ID):
defmodule MyWeb.GraphQL.Schema do
...
subscription do
field :user_download_ready, :download_link do
config(fn args, context ->
{:ok, topic: "user_downloads:#{context[:context][:user_id]}"}
end)
resolve(fn link, _, _res ->
{:ok, link}
end)
end
field :download_ready, :download_link do
# Unique download ID of the job
arg(:download_id, non_null(:string))
config(fn args, _context ->
{:ok, topic: "downloads:#{args.download_id}"}
end)
resolve(fn link, _, _res ->
{:ok, link}
end)
end
end
end
If we would want to test this setup without initiating a download request (like sending a mutation) we can use Absinthe.Subscription.publish
to publish new link that will be then resolve with the above resolver function. We can then watch console output on what’s happening.
Absinthe.Subscription.publish(
AppWeb.Endpoint,
%{url: "testurl"},
user_download_ready: "user_downloads:1"
)
If we want to test our new subscription using GraphiQL we need to update our router to point GraphiQL to the right socket module:
# lib/app_web/router.ex
...
scope "/api/graphiql" do
pipe_through [:browser, :authenticate_on_post_only]
forward "/", Absinthe.Plug.GraphiQL,
schema: AppWeb.GraphQL.Schema,
socket: AppWeb.UserSocket
...
And then make sure that we are setting the correct WS URL for the WebSocket connection next to the usual URL in the top part of the GraphiQL. In case of using SSL/TLS this would be:
- URL:
https://localhost:4443/graphql
- WS URL:
wss://localhost:4443/socket
At last we just send the subscription request and wait for the results to appear as they come:
subscription {
userDownloadReady() {
url
}
}
Note that in our case the user is determined by the OAuth 2 bearer token in the Authorization
header in the format of Bearer TOKEN
that we added in the top part of the GraphiQL under the API and WS URLs.