Skip to content

Handler

Lyo Kato edited this page Apr 20, 2022 · 42 revisions

Callbacks

init/2

Check out the following example

@impl Requiem
def init(conn, client) do

  Logger.debug("new connection is established, connection_id: #{conn.dcid}")

  if client.origin == "my-origin.example.com" do

    my_state = %{path: client.path}
    {:ok, conn, state}

  else

    {:stop, 400, :forbidden}

  end
end

It has a role similar to GenServer.init/2, and is called after a successful QUIC handshake.

Arguments

There are two arguments, ConnectionState and ConnectRequest.

If necessary, you can check the values of the ConnectRequest parameters, authority, origin and path, and do whatever you want. In the example above, if the origin is not correct, the connection is disconnected at this point, and if the origin is as expected, the connection is maintained.

The handling of origin is similar to that of CORS in HTTP.

path is the part of the URL path specified when this server is accessed.

If the URL is https://example.org/foobar, then the /foobar part will be included.

You may want to look at this value to see if you can branch the service if necessary.

authority is the set of hostnames and ports specified by the client when accessing this server.

If a connection is made using a URL such as https://example.org:1234/webtransport, it will contain "example.org:1234".

Return value

If there are no problems, return a value like {:ok, conn, state}. It is the same as GenServer.init/1 in that you can freely create and return a state. The only difference is that the ConnectionState structure is passed around as well.

If there is a problem and you want to shut down the connection here, it will return data of the form {:stop, error_code, reason}.

The error_code and reason used here will be used in the CLOSE FRAME to be sent to the client.

  @callback init(conn :: Requiem.ConnectionState.t(), client :: Requiem.ClientIndication.t()) ::
              {:ok, Requiem.ConnectionState.t(), any}
              | {:ok, Requiem.ConnectionState.t(), any, timeout | :hibernate}
              | {:stop, non_neg_integer, atom}

handle_info/3

This one is also somewhat understandable if you look at the example.

@impl Requiem
def handle_info({:my_event, my_param}, conn, state) do
   do_something(my_param)
  {:noreply, conn, state}
end

You can think of it as almost the same as GenServer.handle_info/2, except that it pulls around the ConnectionState structure for the arguments and return value. This callback is called when the process receives a message.

  @callback handle_info(
              request :: term,
              conn :: Requiem.ConnectionState.t(),
              state :: any
            ) ::
              {:noreply, Requiem.ConnectionState.t(), any}
              | {:noreply, Requiem.ConnectionState.t(), any, timeout | :hibernate}
              | {:stop, non_neg_integer, atom}

handle_cast/3

As with handle_info/2 above, you can think of this function as having almost the same role as the corresponding GenServer.handle_cast/2.

@impl Requiem
def handle_cast({:my_event, my_param}, conn, state) do
   do_something(my_param)
  {:noreply, conn, state}
end
  @callback handle_cast(
              request :: term,
              conn :: Requiem.ConnectionState.t(),
              state :: any
            ) ::
              {:noreply, Requiem.ConnectionState.t(), any}
              | {:noreply, Requiem.ConnectionState.t(), any, timeout | :hibernate}
              | {:stop, non_neg_integer, atom}

handle_call/4

@impl Requiem
def handle_call({:my_event, my_param}, from, conn, state) do
  {:reply, conn, state}
end

This is also the same as handle_info/2 and handle_cast/2 above.

  @callback handle_call(
              request :: term,
              from :: pid,
              conn :: Requiem.ConnectionState.t(),
              state :: any
            ) ::
              {:noreply, Requiem.ConnectionState.t(), any}
              | {:noreply, Requiem.ConnectionState.t(), any, timeout | :hibernate}
              | {:reply, any, Requiem.ConnectionState.t(), any}
              | {:reply, any, Requiem.ConnectionState.t(), any, timeout | :hibernate}
              | {:stop, non_neg_integer, atom}

terminate/3

Similarly, there is a function called terminate/3.

@impl Requiem
def terminate(reason, conn, state) do
  :ok
end
  @type terminate_reason :: :normal | :shutdown | {:shutdown, term} | term

  @callback terminate(
              reason :: terminate_reason,
              conn :: Requiem.ConnectionState.t(),
              state :: any
            ) :: any

handle_stream/4

A function that can process data received in a stream.

@impl Requiem
def handle_stream(stream_id, data, conn, state) do

  case do_something(stream_id, data) do
    {:ok, good_result} -> {:ok, conn, state}
    {:error, :bad_result} -> {:stop, 400, :bad_request}
  end

end

Arguments

You may be familiar with conn and state by now. The stream_id and data specified in the first two arguments are passed.

Return value

If there is no problem, return data in the form of {:ok, conn, state}. If there is a problem and you want to close the connection, return data in the form of {:stop, error_code, reason}.

  @callback handle_stream(
              stream_id :: non_neg_integer,
              data :: binary,
              conn :: Requiem.ConnectionState.t(),
              state :: any
            ) ::
              {:ok, Requiem.ConnectionState.t(), any}
              | {:ok, Requiem.ConnectionState.t(), any, timeout | :hibernate}
              | {:stop, non_neg_integer, atom}

handle_dgram/3

It can handle data sent as datagrams. It is the same as handle_stream/4 except that there is no such thing as a stream_id.

@impl Requiem
def handle_dgram(data, conn, state) do

  case do_something(stream_id, data) do
    {:ok, good_result} -> {:ok, conn, state}
    {:error, :bad_result} -> {:stop, 400, :bad_request}
  end

end
  @callback handle_dgram(
              data :: binary,
              conn :: Requiem.ConnectionState.t(),
              state :: any
            ) ::
              {:ok, Requiem.ConnectionState.t(), any}
              | {:ok, Requiem.ConnectionState.t(), any, timeout | :hibernate}
              | {:stop, non_neg_integer, atom}

Methods

The following functions are available in your handler.

close/0

It is possible to disconnect by returning {:stop, error_code, reason} in the return value of each callback. However, there may be times when you want to disconnect in an asynchronous process other than the callback timing.

In such a case, call this close function.


close/2

There is also a version of close where error_code and reason can be passed. The above version is treated as a normal close, but this version can express an application error.

close(error_code, reason)

stream_send/3

Use this when you want to send data to a client through a stream.

stream_send(stream_id, data, fin_stream_flag)

You can use it this way, but be careful about the value of stream_id.

See Stream for details.


dgram_send/1

This can be used when you want to send a datagram instead of a stream.

dgram_send(data)

ConnectionState

The actual state of the conn passed in each callback is a structure defined in the Requiem.ConnectionState module.

It has the following fields

  • conn.scid
  • conn.dcid
  • conn.odcid

These are the connection IDs used in the QUIC frame, and are the ones specified when the connection is initiated. odcid is what was specified before validation by token using the retry frame.

It also has the address of the peer.

  • conn.address

This is the structure of the Requiem.Address module.

  • conn.address.host
  • conn.address.port

In this way, you can access information about the other party's connection.


ConnectRequest

In addition to ConnectionState, there is one other thing that you need to know. It is the ConnectRequest passed as the second argument to init/2.

@impl Requiem
def init(conn, client) do
  my_state = %{}
  {:ok, conn, my_state}
end

The set of parameters included when the Client sends a WebTransport connection request.

Here, you can check the values of authority, path, and origin, and disconnect if there is a problem.

@impl Requiem
def init(conn, client) do
  if client.origin == "example.org"  do
    my_state = %{path: client.path}
    {:ok, conn, my_state}
  else
    {:stop, 400, :shutdown}
  end
end