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!