When do I need to `halt()` a connection?

Apologies if this question has been asked before, I couldn’t find any reference to the general concept in my searches.

I’m mainly wondering if there is a rule of thumb on whether or not halt/1 is required when piping a connection. I assume the functionality is bundled in with certain functions (e.g. render()), or perhaps returning any conn will result in it being closed? I am just looking for some clarification/confirmation as to whether a connection is halted automatically by default, or if there are certain situations where it must be called manually.

For example, I’m building an API where a delete request results in a 204 No Content response. Is it sufficient conn |> put_status(:no_content), or do I need to pipe into halt/1 as well? Looking for a “rule of thumb” sort of answer here. Teach a man to fish and all that.

Secondary questions: Using the Request Logger in the LiveDashboard, I am able to view some data about the response. If I see a line like [info] Sent 401 in 175ms, does that mean the connection has been closed? Is the LiveDashboard the best way to view a list of currently-opened connections?

Tertiary questions: Am I worrying about nothing? Do I even have to concern myself with whether or not Phoenix is closing the connections automatically? Will I get caught with my pants down because I accidentally opened a bunch of connections, or is this something that Phoenix automatically will take care of for me?

Thanks a bunch :slight_smile:

If you read the documentation, you can find that halt is used to stop the connection at said plug. This is very useful for prechecks, easiest example being routes that require authentication, if it fails halt and return 401.

Why you need to explicitly halt? That is because each plug accepts a connection and returns one, you can add something you want to render in a plug and continue the pipeline no problems, you could even override what is rendered.

3 Likes

Without an explicit halt/1, Phoenix will eventually close the connection automatically – but only after it goes through the whole pipeline of plugs.

The :authenticate plug will be invoked before the action. If the plug calls Plug.Conn.halt/1 (which is by default imported into controllers), it will halt the pipeline and won’t invoke the action.

defmodule MyAppWeb.UserController do
  use MyAppWeb, :controller

  plug :authenticate, usernames: ["jose", "eric", "sonny"]

  def show(conn, params) do
    # authenticated users only
  end

  defp authenticate(conn, options) do
    if get_session(conn, :username) in options[:usernames] do
      conn
    else
      conn |> redirect(to: "/") |> halt()
    end
  end
end

source: Phoenix.Controller docs

If you were to pipe into a halt here, you may be surprised to find that you would not actually be sending the 204 response to the client because the conn would not reach the rendering logic. Check out this article on Halting Plugs in Phoenix.

tl;dr regarding a rule of thumb, my intuition for when to use halt is when I would reach for a bouncer pattern/early return/short circuit when handling a request.

A mental model I find useful is railway oriented programming and coding for “the happy path”. In this context, a conn is like the train, the pipeline is like the railway route aka “the happy path”, and plugs are segments of the route guarded by a checkpoint. Trains putter along “the happy path” route so long as the each checkpoint returns a success. But if for whatever reason, a checkpoint aka plug returns an error via a halt, then the train/conn gets directed off the route/pipeline. With this in mind, you only need to concern yourself with an explicit halt when you want to stop the train from completing its route aka you don’t want the conn to run through any subsequent plugs.

3 Likes

Gotcha, I figured that was the case.

As you mentioned, I tried a response with just put_status and no response (which I swear I had tried before…) and as you said, the connection just hung.

So that basically answers my main concern: I’m not going to be surprised by any connections remaining open. If I get a response, I can safely assume the plug has run its course and has closed.

Thanks for the replies.