Simon Tirant's blog

đŸ‡«đŸ‡· 🇬🇧
← All blog posts
Message d'erreur animé

Créez vos propres codes HTTP dans Phoenix avec Plug

by Simon Tirant (8 min read)

Tagged as elixir phoenix plug web http

This post also exists in english

Par dĂ©faut - Ă  l’intĂ©rieur de votre application Phoenix - la plupart des exceptions levĂ©es sont des erreurs 500: la tristement cĂ©lĂšbre Internal Server Error, indiquant que le dĂ©veloppeur du site est un vilain programmeur qui a laissĂ© des fautes dans son code et qui a oubliĂ© de se relire. Mais voilĂ , parfois c’est aussi la faute du visiteur! Alors s’il vous plaĂźt, ne mordez pas la main qui vous nourrit avec du si bon contenu et naviguez en sachant oĂč vous allez!

Donc, dans mon cas je voulais lever deux autres types d’exceptions que la 500: la premiĂšre Ă©tait la fameuse 404 Not Found, mais j’ai aussi dĂ©cidĂ© de crĂ©er une erreur personnalisĂ©e et plus spĂ©cifique 461 No Blog Posts Found . Voici mon histoire 
 📖

Vieux livre s'ouvrant

404 101 🧑‍🎓

Pour la premiĂšre d’entre elles, je voulais lever une exception quand un article spĂ©cifique n’était pas trouvĂ© pour une langue en particulier. Par exemple, je gĂšre la traduction sur ce site avec le chemin de l’url; regardez la barre d’adresse: l’article que vous ĂȘtes en train de lire actuellement a un nom de domaine blog.simontirant.dev / puis la route post / puis l’id de l’article (custom_http_codes) / enfin l’attribut de langue qui peut ĂȘtre soit french soit english. Si vous dĂ©cidez - pour quelque raison que ce soit - de changer ce dernier attribut du chemin pour Ă©crire silbo Ă  la place (n.b.: le silbo est une antique langue sifflĂ©e utilisĂ©e Ă  Gomera, une des Ăźles Canaries), alors le serveur lĂšvera cette exception 404 Not Found - Ă  la place de la 500 par dĂ©faut - car, bien que je puisse toujours apprendre le silbo en ligne et enregistrer mon article tech dans un fichier audio pour que vous puissiez l’écouter dans un lecteur web, je ne suis pas sĂ»r qu’il y ait un public pour ça.

ConcrĂštement, ma fonction show/2 situĂ©e dans mon BlogController appelle une autre fonction, situĂ©e dans le module Blog, nommĂ©e get_post_by_id_and_lang!/2, en lui passant comme paramĂštres les attributs d’url concernant la langue et l’id de l’article. Cette fonction Blog.get_post_by_id_and_lang!/2 est ensuite chargĂ©e de retrouver l’article par son id et sa langue (car deux articles peuvent avoir le mĂȘme id s’ils sont dans une langue diffĂ©rente), si nil elle lĂšve une exception appelĂ©e NotFoundError qui a Ă©tĂ© auparavant dĂ©clarĂ©e dans son propre module. Voici un extrait de mon code ci-dessous:

defmodule NimbleBlog.Blog do

  @posts Enum.sort_by(@posts, & &1.date, {:desc, Date})

  def all_posts, do: @posts

  defmodule NotFoundError, do: defexception([:message, plug_status: 404])

  @doc """
  Returns a specific %Post{} from the combination of its id and language.
  Raise if nothing is found.

  #### Example:
      iex> get_post_by_id_and_lang!("post_1", "english")
      %Post{}

      iex> get_post_by_id_and_lang!("post_1", "german")
      ** (NotFoundError) post with both id="post_1" and lang="german" not found
  """
  @spec get_post_by_id_and_lang!(String.t(), String.t()) :: %Post{} | NotFoundError
  def get_post_by_id_and_lang!(id, lang) do
    Enum.find(all_posts(), &(&1.id == id && &1.lang == lang)) ||
      raise NotFoundError, "post with both id=#{id} and lang=#{lang} not found"
  end
end

P.S.: ce blog utilise la librairie NimblePublisher pour rendre des articles de blog Ă  partir de fichiers Markdown situĂ©s dans un fichier du serveur. Comme je n’utilise pas de base de donnĂ©es, il n’y a donc pas de fonctions Ecto telles que Repo.get!/3, mais des fonctions venant du module Enum Ă  la place. Lisez la premiĂšre partie de mon article sur NimblePublisher ici.

Comme nous pouvons le voir, l’exception dĂ©clarĂ©e comme NotFoundError est liĂ©e Ă  un code HTTP 404 classique, ceci grĂące Ă  l’option plug_status: 404 que j’ai ajoutĂ© juste aprĂšs :message dans la dĂ©claration de mon exception. La 404 est un code HTTP bien connu et Plug la connait dĂ©jĂ , regardez au tout dĂ©but du module Plug.Conn.Status, situĂ© dans le dossier des deps:

  statuses = %{
    ...,
    400 => "Bad Request",
    401 => "Unauthorized",
    402 => "Payment Required",
    403 => "Forbidden",
    404 => "Not Found",
    405 => "Method Not Allowed",
    406 => "Not Acceptable",
    407 => "Proxy Authentication Required",
    408 => "Request Timeout",
    409 => "Conflict",
    410 => "Gone",
    411 => "Length Required",
    412 => "Precondition Failed",
    413 => "Request Entity Too Large",
    414 => "Request-URI Too Long",
    415 => "Unsupported Media Type",
    416 => "Requested Range Not Satisfiable",
    417 => "Expectation Failed",
    418 => "I'm a teapot",
    421 => "Misdirected Request",
    422 => "Unprocessable Entity",
    423 => "Locked",
    424 => "Failed Dependency",
    425 => "Too Early",
    426 => "Upgrade Required",
    428 => "Precondition Required",
    429 => "Too Many Requests",
    431 => "Request Header Fields Too Large",
    451 => "Unavailable For Legal Reasons",
    ...
  }

Cela signifie que si ma fonction Ă©choue Ă  retrouver un article de blog pour cet id et cette langue - sous-entendant qu’au moins un des deux arguments de l’url, situĂ©s aprĂšs la route post, est faux - alors l’ exception NotFoundError est levĂ©e, appelant l’erreur 404 de Plug, puis Phoenix redirige le visiteur vers la page 404.html.heex que j’ai crĂ©Ă© Ă  l’intĂ©rieur de mon dossier error_html, lui-mĂȘme situĂ© dans le dossier des controllers. Pour appeler ce template en particulier, il faut se rendre dans la vue error_html.ex (toujours dans le dossiers des controllers) et ajouter embed_templates("error_html/*") - Ă  la place de la fonction render/2 dĂ©jĂ  prĂ©sente. Avec ceci, Phoenix pourra afficher les templates du dossier error_html pour vos pages d’erreur personnalisĂ©es (lien vers la page dĂ©diĂ©e de la documentation).

Ce mĂ©canisme consistant Ă  appeler le status 404 de Plug depuis une exception, Phoenix le fait lui aussi pour une NoRouteError - une exception nativement levĂ©e par le module Phoenix.Router quant une route simple n’existe tout simplement pas: par exemple, si vous Ă©crivez /pist au lieu de /post dans l’url. Dans notre cas, la langue et l’id Ă©tant des paramĂštres et non pas une route, un terme incorrect sur ceux-ci aurait nativement appelĂ© l’erreur 500 et non pas 404; mais j’aurais donc tout aussi bien pu dĂ©cider d’importer et de lever dans mon module Blog l’exception native Phoenix.Router.NotFoundError, au lieu de crĂ©er ma propre exception, car elle appelle aussi le status 404 de Plug.

Mais si vous avez rĂ©ellement regardĂ©, comme je l’ai demandĂ©, en haut du module Plug.Conn.Status de n’importe quelle projet Phoenix, vous devez avoir vu ceci juste avant la liste des status HTTP :

  custom_statuses = Application.compile_env(:plug, :statuses, %{})

Vous savez ce que ça veut dire ? Ça veut dire que, mĂȘme si la liste des codes HTTP a dĂ©jĂ  anticipĂ© des blagues Ă  l’intention des dĂ©veloppeurs - telles que 418 I'm a teapot ou, ma prĂ©fĂ©rĂ©e, 451 Unavailable For Legal Reasons 
 la censure d’état dans Fahrenheit 451, vous l’avez ? - afin qu’ils puissient rigoler un peu, elle laisse tout un tas de codes disponibles afin de crĂ©er vos propres status bizarres.

PrĂȘt Ă  jouer avec les nombres ? 🧼

Donc, j’ai aussi besoin de lever une erreur quand le rĂ©sultat d’un filtre par tags dans une langue en particulier est vide. Ainsi, si vous cliquez, par exemple, sur le tag tricot đŸ§¶ puis sur le drapeau anglais 🇬🇧 pour filtrer uniquement les articles en anglais sur le tricot, et bien vous allez lever mon code HTTP personnalisĂ© 461 No Blog Posts Found 
 Car je n’ai pas encore Ă©crit d’article en anglais sur le sujet. Et parce qu’il s’agit ici d’un blog tech, l’exception aurait Ă©tĂ© levĂ©e en français đŸ‡«đŸ‡· aussi ! Ce status, comme le 404, va ensuite appeler la vue 461.html.heex situĂ©e dans le dossier error_html pour vous expliquer que le serveur n’a trouvĂ© aucun article pour votre requĂȘte.

Mais 
 Comment ai-je fait ceci ? Est-ce de la magie noire ? Booooâ€ŠđŸ•·ïžđŸ”źđŸȘ„đŸ§ż

Et bien, en vĂ©ritĂ© non. Je l’ai juste fait en utilisant la fonction Config.config/3 sur les status de Plug Ă  l’intĂ©rieur du fichier config.exs dans le rĂ©pertoire config:

import Config

config :plug, :statuses, %{461 => "No Blog Posts Found"}

De cette façon, lors de la compilation, Plug va construitre sa map de status que nous avons vue plus tĂŽt avec le code HTTP 404, mais en incluant cette fois-ci les status personnalisĂ©s que nous avons passĂ© dans le fichier config.exs. Comme vous l’aurez peut-ĂȘtre compris, il est nĂ©cessaire de recompiler la dĂ©pendance Plug aprĂšs cette modification et, dans mon cas, j’ai dĂ» utiliser mix deps.compile plug --force, sans quoi mix retournait une erreur si je n’ajoutais pas l’option --force. AprĂšs cela, je peux ensuite faire ce qui suit dans mon module Blog afin de filtrer les articles de blog:

defmodule NimbleBlog.Blog do
  @posts Enum.sort_by(@posts, & &1.date, {:desc, Date})

  def all_posts, do: @posts

  defmodule NoBlogPostsError, do: defexception([:message, plug_status: 461])

  @doc """
  Gets the list of posts filtered by a specific tag or language or a combination of both.

  Example:
      iex> list_posts_by_filters!(%{tag: "elixir", lang: "english"})
      [%Post{lang: "english", tags: ["elixir", ...]}, ...]

      iex> list_posts_by_filters!(%{tag: "cooking", lang: "sumerian"})
      ** (NoBlogPostsError) posts no found with tag cooking in sumerian language.
  """
  @spec list_posts_by_filters!(%{tag: String.t() | nil, lang: String.t() | nil}) ::
          [%Post{}] | NoBlogPostsError
  def list_posts_by_filters!(%{"tag" => "all", "lang" => "all"}), do: all_posts()
  def list_posts_by_filters!(%{"tag" => "all", "lang" => lang}), do: lang |> list_posts_by_lang!()
  def list_posts_by_filters!(%{"tag" => tag, "lang" => "all"}), do: tag |> list_posts_by_tag!()

  def list_posts_by_filters!(%{"tag" => tag, "lang" => lang}) do
    all_posts()
    |> Enum.filter(&(lang == &1.lang))
    |> Enum.filter(&(tag in &1.tags))
    |> case do
      [] -> raise NoBlogPostsError, "posts no found with tag #{tag} in #{lang} language."
      posts -> posts
    end
  end

  defp list_posts_by_tag!(tag) do
    case Enum.filter(all_posts(), &(tag in &1.tags)) do
      [] -> raise NoBlogPostsError, "posts with tag=#{tag} not found"
      posts -> posts
    end
  end

  defp list_posts_by_lang!(lang) do
    case Enum.filter(all_posts(), &(lang == &1.lang)) do
      [] -> raise NoBlogPostsError, "posts for #{lang} language not found"
      posts -> posts
    end
  end
end

Comme pour le code 404, j’ai dĂ©clarĂ© mon exception NoBlogPostsError Ă  l’intĂ©rieur de son propre module et j’ai utilisĂ© cette fois-ci l’option plug_status: 461 afin d’appeler le status personalisĂ© que nous avons paramĂ©trĂ© auparavant dans config.exs. Les fonctions en charge de retourner la liste des %Post{} - filtrĂ©e par langue, par tag ou les deux - peuvent ensuite lever le module d’exception NoBlogPostsError dans le cas oĂč la liste des %Post{} serait vide.

N.B.: cette mĂ©thode, pour ajouter des codes personnalisĂ©s Ă  partir de ceux qui ne sont pas utilisĂ©s par la liste des codes HTTP standard, peut aussi ĂȘtre utilisĂ©e pour crĂ©er un message de status diffĂ©rent pour un code existant dĂ©jĂ . Ainsi, vous pouvez modifier le 404 Not Found pour un effrontĂ© 404 Found But Don't Want To Show It To You; ou encore 451 Sorry But It Burned!. Super!

Go up arrow