Generic graphql HTTP and websocket transports for Cowboy.
It supports following transport protocols:
- HTTP POST application/x-www-form-urlencoded
- HTTP POST application/json
- HTTP GET with data passed as URL query string
- WebSocket graphql_ws
- WebSocket apollo
For HTTP transport the response payload can be delivered as:
application/json
- only single response will be delivered. See spec.multipart/mixed
delivered in chunks (each chunk asapplication/json
). Multiple responses can be delivered for subscriptions. See spec.
It supports both request-response as well as subscriptions. It does not dictate the specific GraphQL executor implementation (however, https://github.com/jlouis/graphql-erlang is assumed).
The idea is to define a cowboy_graphql
behaviour callback module and the same code can be used for
any transport protocol.
The cowboy_graphql
behaviour module have to implement following callbacks.
connection(cowboy_req:req(), Params :: any()) ->
{ok, handler_state()}
| {error, auth_error() | protocol_error() | other_error()}.
Function is called when the initial HTTP request is received (HTTP headers for HTTP or
Connection: Upgrade; Upgrade: websocket
request for WebSocket).
This callback can be used to, eg, perform the authentication, inspect the HTTP headers, peer IP etc.
This module initiates the static fields in the state, however it is not guaranteed to be executed in the same process as all the other callbacks. So, don't start timers/monitors in this callback.
init(Params :: json_object(), TransportInfo :: transport_info(), State :: handler_state()) ->
{ok, Params :: json_object(), handler_state()}
| {error, auth_error() | other_error(), handler_state()}.
Function is called when the connection is established (for HTTP - imediately after connection
,
for WebSocket - when ConnectionInit
message is received).
The TransportInfo
parameter provides some details about the transport and its features that was
chosen for this connection (eg, is it WebSocket or HTTP? was it POST or GET? etc).
This callback can be used to set-up all the timers/monitors, register the process in process registry etc.
handle_request(
request_id(),
OperationName :: binary() | undefined,
Query :: unicode:chardata(),
Variables :: json_object(),
Extensions :: json_object(),
State :: handler_state()) ->
{noreply, handler_state()}
| {reply, result() | [result()], handler_state()}
| {error, request_validation_error() | other_error(), handler_state()}.
-type result() :: {
Id :: request_id(),
Done :: boolean(),
Data :: json_object() | undefined,
Errors :: [graphql_error()],
Extensions :: json_object()
}.
Is called when GraphQL query or subscription is received (for HTTP - we read and parse body,
for WebSocket - when Subscribe
message received).
The actual GraphQL request should be parsed, validated and executed here.
This callback may either:
- return the result immediately
- initiate the subscription or execute request asynchronously (storing the
request_id()
as correlation key) and return the result(s) fromhandle_info
The Done
flag of the result()
signals whether the query is done producing results (should be
always true
for query
and mutation
) or more results could be produced (for subscription
).
handle_cancel(Id :: binary(), handler_state()) ->
{noreply, handler_state()}
| {error, other_error(), handler_state()}.
Is called when client requested the subscription cancellation (for HTTP - see handle_info
,
for WebSocket - when client sends Complete
).
handle_info(Msg :: any(), State :: handler_state()) ->
{noreply, handler_state()}
| {reply, result() | [result()], handler_state()}
| {error, other_error(), handler_state()}.
Is called when the Erlang process that represents the connection receives regular messages from other processes (eg, from pub-sub system).
For HTTP transport when json
response encoding is chosen, reply
or error
can be returned
only once. If the result()
has Done
flag set to false
, handle_cancel/2
will be called
immediately for json
. For multipart
method there is no such limitation.
-callback terminate(
Reason :: normal | atom() | tuple(),
State :: handler_state()
) -> ok.
Called when connection is about to be closed.
HTTP and WebSocket handlers should reside on different URLs. Any protocol can be disabled.
start() ->
CallbackMod = my_cowboy_graphql_impl,
Opts = #{..}, % options passed to `connection(_, Opts)`
Routes = [
{"/api/http", cowboy_graphql_http_handler,
cowboy_graphql:http_config(
CallbackMod, Opts, #{json_mod => jsx,
accept_body => [json, x_www_form_urlencoded],
response_types => [json, multipart],
allowed_methods => [post, get],
max_body_size => 5 * 1024 * 1024})};
{"/api/websocket", cowboy_graphql_ws_handler,
cowboy_graphql:ws_config(
CallbackMod, Opts, #{json_mod => jsx,
protocols => [graphql_ws, apollo],
max_frame_size => 5 * 1024 * 1024})}
],
Dispatch = cowboy_router:compile([{'_', Routes}]),
{ok, _} = cowboy:start_clear(
?MODULE,
#{
max_connections => 1024,
socket_opts => [{port, 8080}]
},
#{env => #{dispatch => Dispatch}}
).
accept_body => [json | x_www_form_urlencoded]
- for POST requests, the list of allowed requestContent-Type
response_types => [json | multipart]
- the list of allowed requestAccept
encoding types for response body.json
allows only single response whilemultipart
supports multiple responses (eg, subscriptions producing multiple results)allowed_methods => [post | get]
- allowed HTTP methodsjson_mod => module()
- JSON library module. Should exportencode(cowboy_graphql:json()) -> iodata()
anddecode(binary()) -> cowboy_graphql:json()
max_body_size => pos_integer()
- maximum allowed request body sizeheartbeat => pos_integer()
(milliseconds) - it only works formultipart
streaming method and it works by sending empty JSON object{}
multipart bodies periodically to make sure connection is not closed by client or proxy; it is a no-op forjson
method and ifhandle_request
callback returns{reply, result(), ..}
withDone
flag set totrue
(because connection is closed after that). It does not make sense to setheartbeat
to be smaller thantimeout
. Default: disabled.timeout => timeout()
(milliseconds) - close the connection after this many milliseconds no matter what (terminate(timeout, ...)
callback will be called). Set toinfinity
to disable. Default: 10 minutes
Both heartbeat
and timeout
timers are only started after handle_call
callback returns noreply
.
So the timeout is "soft". If your handle_call
is slow, it won't be interrupted.
For accept_body
and response_types
if no Content-Type
/Accept
header provided in the request,
the first element of the list from the option will be used. Eg, if response_types => [json, multipart]
and there is no Accept
header in the request, then json
(application/json
) will be chosen.
protocols => [graphql_ws | apollo]
- list of GraphQL-over-websocket protocols to accept; it will be negotiated viaSec-WebSocket-Protocol
header. If no such header set, the first in the list will be used. Default:[graphql_ws, apollo]
json_mod => module()
same as in HTTP. Default:jsx
max_frame_size => pos_integer()
(bytes) - maximum allowed size of single WebSocket request frame Default: 5 MBheartbeat => pos_integer()
(milliseconds) - send WebSocketping
frames (not GraphQL sub-protocol pings!) to the client periodically; there is no consequences currently if client does not reply unlessidle_timeout
is set. Default: disabledidle_timeout => timeout()
(milliseconds) - close the WebSocket (and callterminate(timeout, ...)
callback) if no data was received from client in this many milliseconds. Default: 10 minutes
It is highly recommended to not set idle_timeout
to infinity, but to set both idle_timeout
and
heartbeat
. Makes sense to have heartbeat
smaller than idle_timeout
.
GraphQL sub-protocol pings are answered automatically under the hood.