Créez vos propres codes HTTP dans Phoenix avec Plug
by Simon Tirant (8 min read)
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 ⊠đ
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!