Trilha Phoenix · Páginas de erro
Mix.install(
[
{:phoenix, "~> 1.8.0"},
{:phoenix_live_view, "~> 1.1"},
{:phoenix_ecto, "~> 4.6"},
{:ecto_sql, "~> 3.13"},
{:postgrex, ">= 0.0.0"},
{:bandit, "~> 1.5"},
{:jason, "~> 1.4"},
{:kino, "~> 0.14"}
],
consolidate_protocols: false,
config: [
phoenix: [json_library: Jason, plug_init_mode: :runtime],
hello: [
{HelloWeb.Endpoint,
[
adapter: Bandit.PhoenixAdapter,
http: [ip: {127, 0, 0, 1}, port: 4010],
server: true,
debug_errors: true,
secret_key_base: String.duplicate("trilha-phoenix-", 5),
render_errors: [formats: [html: HelloWeb.ErrorHTML, json: HelloWeb.ErrorJSON], layout: false]
]}
]
]
)
defmodule Trilha do
@moduledoc """
Apoio deste notebook: faz requisições ao endpoint, mostra a resposta que
foi enviada (inclusive quando a requisição termina em exceção), grava os
arquivos de template, confere os exercícios e guarda as perguntas e as soluções.
Não faz parte do Phoenix. Você não precisa ler este módulo para seguir o guia.
"""
@endpoint HelloWeb.Endpoint
@router HelloWeb.Router
# --- arquivos e configuração -------------------------------------------------
@doc "A pasta que faz o papel de lib/hello_web/controllers no notebook."
def pasta, do: Path.join(System.tmp_dir!(), "trilha_phoenix_05/lib/hello_web/controllers")
@doc "Grava um arquivo de template dentro de pasta/0."
def arquivo(caminho, conteudo) do
destino = Path.join(pasta(), caminho)
File.mkdir_p!(Path.dirname(destino))
File.write!(destino, conteudo)
Kino.Markdown.new(
"Arquivo gravado: `lib/hello_web/controllers/#{caminho}`.\n\n" <>
"O `embed_templates` lê os arquivos quando o módulo é compilado: avalie de novo a célula do `HelloWeb.ErrorHTML`."
)
end
@doc "Altera a configuração do endpoint, como uma linha de config/dev.exs."
def config(opcoes) do
atual = Application.get_env(:hello, @endpoint)
Application.put_env(:hello, @endpoint, Keyword.merge(atual, opcoes))
linha = opcoes |> inspect() |> String.slice(1..-2//1)
Kino.Markdown.new(
"`config :hello, HelloWeb.Endpoint, #{linha}`\n\n" <>
"Agora avalie a célula do endpoint, logo abaixo, para que ele seja compilado com essa configuração."
)
end
# --- requisições -----------------------------------------------------------
@doc """
Despacha uma requisição para o endpoint, sem passar pela rede, e devolve a
resposta que foi enviada: status, content-type, corpo e a exceção, se houve.
Opções: `accept:` (o cabeçalho Accept) e `params:` (o corpo de um POST).
"""
def resposta(metodo, caminho, opcoes \\ []) do
descartar_respostas_antigas()
conn =
case opcoes[:accept] do
nil -> Phoenix.ConnTest.build_conn()
accept -> Plug.Conn.put_req_header(Phoenix.ConnTest.build_conn(), "accept", accept)
end
{final, excecao} =
try do
{Phoenix.ConnTest.dispatch(conn, @endpoint, metodo, caminho, opcoes[:params]), nil}
rescue
erro -> {nil, desembrulhar(erro)}
end
# Para NoRouteError (e só para ela), o Phoenix envia a página de erro e
# devolve o conn sem relançar a exceção. Reconstruímos a exceção aqui.
excecao =
if excecao == nil and final != nil and final.status in [nil, 404] and rota(metodo, caminho) == :error do
Phoenix.Router.NoRouteError.exception(conn: final, router: @router)
else
excecao
end
# Cada resposta enviada pelo adaptador de testes chega como uma mensagem.
receive do
{ref, {status, cabecalhos, corpo}} when is_reference(ref) ->
tipo = cabecalhos |> List.keyfind("content-type", 0, {nil, nil}) |> elem(1)
%{status: status, content_type: tipo, corpo: corpo, excecao: excecao}
after
0 -> %{status: nil, content_type: nil, corpo: nil, excecao: excecao}
end
end
defp descartar_respostas_antigas do
receive do
{ref, {status, cabecalhos, _}} when is_reference(ref) and is_integer(status) and is_list(cabecalhos) ->
descartar_respostas_antigas()
{:plug_conn, :sent} -> descartar_respostas_antigas()
after
0 -> :ok
end
end
defp desembrulhar(%Plug.Conn.WrapperError{reason: motivo}), do: desembrulhar(motivo)
defp desembrulhar(erro), do: erro
defp rota(metodo, caminho) do
so_caminho = caminho |> String.split("?") |> hd()
Phoenix.Router.route_info(@router, verbo(metodo), so_caminho, "localhost")
rescue
_ -> :error
end
defp verbo(metodo), do: metodo |> to_string() |> String.upcase()
@doc "A resposta veio da página de depuração do Plug.Debugger?"
def depuracao?(%{content_type: tipo, corpo: corpo}) do
(is_binary(tipo) and String.starts_with?(tipo, "text/markdown")) or
(is_binary(corpo) and String.contains?(corpo, ~s(name="error-status")))
end
defp quem_respondeu(%{excecao: nil}), do: "a action do controller, sem exceção"
defp quem_respondeu(resposta) do
cond do
depuracao?(resposta) ->
"a página de depuração do `Plug.Debugger` (o endpoint está com `debug_errors: true`)"
is_binary(resposta.content_type) and String.contains?(resposta.content_type, "json") ->
"a view de erro `HelloWeb.ErrorJSON`"
true ->
"a view de erro `HelloWeb.ErrorHTML`"
end
end
@doc "Faz a requisição e mostra a exceção, quem respondeu e a resposta enviada."
def mostrar(metodo, caminho, opcoes \\ []) do
if is_binary(caminho) and String.starts_with?(caminho, "/") do
r = resposta(metodo, caminho, opcoes)
titulo =
"### #{verbo(metodo)} #{caminho}" <>
if(opcoes[:accept], do: " (Accept: `#{opcoes[:accept]}`)", else: "")
excecao =
case r.excecao do
nil -> "**Exceção:** nenhuma."
erro -> "**Exceção:** `" <> String.replace(Exception.format_banner(:error, erro), "`", "'") <> "`"
end
Kino.Markdown.new(
Enum.join(
[
titulo,
excecao,
"**Quem escreveu a resposta:** " <> quem_respondeu(r),
tabela("**A resposta enviada**", [{"status", r.status}, {"content-type", r.content_type}]),
"**Corpo**" <> corpo_md(r)
],
"\n\n"
)
)
else
Kino.Markdown.new("O caminho precisa começar com `/`, por exemplo `/hello`.")
end
end
defp corpo_md(%{corpo: nil}), do: ": nenhum corpo foi enviado."
defp corpo_md(%{corpo: corpo, content_type: tipo}) do
limite = 1200
{texto, aviso} =
if String.length(corpo) > limite,
do: {String.slice(corpo, 0, limite), " (os primeiros #{limite} caracteres de #{String.length(corpo)})"},
else: {corpo, ""}
linguagem =
cond do
tipo == nil -> ""
String.contains?(tipo, "json") -> "json"
String.contains?(tipo, "html") -> "html"
true -> "text"
end
aviso <> "\n\n" <> cerca(texto, linguagem)
end
defp tabela(titulo, pares) do
linhas =
Enum.map(pares, fn {campo, valor} ->
texto = valor |> inspect(limit: 30, printable_limit: 300) |> String.replace("|", "\\|") |> String.replace("`", "'")
"| `#{campo}` | `#{texto}` |"
end)
Enum.join([titulo, "", "| Campo | Valor |", "| --- | --- |" | linhas], "\n")
end
defp cerca(texto, linguagem) do
marca = String.duplicate("`", 3)
marca <> linguagem <> "\n" <> String.trim(texto) <> "\n" <> marca
end
# --- perguntas -------------------------------------------------------------
@doc "Uma pergunta de múltipla escolha, respondida antes de ver a explicação."
def pergunta(id) do
%{texto: texto, opcoes: opcoes, correta: correta, explicacao: explicacao} = Map.fetch!(perguntas(), id)
quadro = Kino.Frame.new()
letras = Enum.map(opcoes, fn {letra, _texto} -> {letra, to_string(letra)} end)
lista = Enum.map_join(opcoes, "\n", fn {letra, opcao} -> "* **#{letra})** #{opcao}" end)
formulario =
Kino.Control.form(
[resposta: Kino.Input.select("Sua resposta", [{nil, "Escolha"} | letras])],
submit: "Conferir"
)
Kino.listen(formulario, fn %{data: %{resposta: resposta}} ->
retorno =
cond do
resposta == nil -> "Escolha uma opção antes de conferir."
resposta == correta -> "**Certo.** " <> explicacao
true -> "**Ainda não.** Releia o trecho acima e tente outra opção."
end
Kino.Frame.render(quadro, Kino.Markdown.new(retorno))
end)
Kino.Layout.grid([Kino.Markdown.new("**Antes de continuar:** " <> texto <> "\n\n" <> lista), formulario, quadro])
end
defp perguntas do
%{
producao: %{
texto: "com `debug_errors: false`, como em produção, o que o navegador recebe em `GET /such/a/wrong/path`, uma rota que não existe?",
opcoes: [
a: "A página de depuração, com a exceção e a lista de rotas.",
b: "Status 404 e o texto `Not Found`, sem nenhuma marcação HTML.",
c: "Status 200 e uma página em branco.",
d: "Um redirecionamento para `/`."
],
correta: :b,
explicacao:
"Sem o `Plug.Debugger`, quem escreve a resposta é a view de erro configurada em `render_errors`. A `HelloWeb.ErrorHTML` gerada só tem uma função `render/2`, que devolve a mensagem padrão do status: para 404, `Not Found`. O status continua 404, porque vem da exceção (`NoRouteError`), não da view."
},
fallback: %{
texto: "para uma rota inexistente, o Phoenix renderiza o template `404` no formato HTML com a view `HelloWeb.ErrorHTML`. Essa view não define nenhuma função chamada `404`. O que acontece?",
opcoes: [
a: "O Phoenix levanta `UndefinedFunctionError` e responde 500.",
b: "O Phoenix chama `render(\"404.html\", assigns)` na mesma view.",
c: "O Phoenix usa uma página de erro própria, embutida no framework.",
d: "A resposta sai com o corpo vazio."
],
correta: :b,
explicacao:
"Ao renderizar um template, o Phoenix procura primeiro uma função com o nome do template e aridade 1 (`404/1`). Se ela não existe, recorre a `render/2`, passando o nome do template junto com o formato: `\"404.html\"`. É essa a função que a `ErrorHTML` gerada define."
},
precedencia: %{
texto: "a `HelloWeb.ErrorHTML` tem o template `404.html.heex` embutido com `embed_templates` (portanto uma função `404/1`) e também uma cláusula `def render(\"404.html\", _assigns)`. Qual das duas produz a página 404?",
opcoes: [
a: "A cláusula de `render/2`, porque foi escrita à mão.",
b: "O template: `render/2` só é chamado quando não existe a função `404/1`.",
c: "Nenhuma: o módulo não compila com as duas.",
d: "Depende da ordem em que aparecem no módulo."
],
correta: :b,
explicacao:
"A busca é sempre a mesma: primeiro a função do template, depois `render/2`. Com o template embutido, a cláusula `render(\"404.html\", _)` nunca é alcançada; vira código morto. O guia diz que a função sobrepõe o template; na versão 1.8.16 acontece o contrário, e você pode conferir isso neste notebook. A recomendação do guia continua valendo: remova a cláusula, para que ninguém se engane lendo o módulo."
},
layout: %{
texto: "a página 404 do guia repete o `<head>` e o cabeçalho em vez de usar `<Layouts.app>`. Por quê?",
opcoes: [
a: "Porque componentes não funcionam dentro de views de erro.",
b: "Para evitar erros em cascata: se o erro tiver vindo do layout, renderizá-lo de novo falharia outra vez.",
c: "Porque o Phoenix proíbe layouts em respostas com status diferente de 200.",
d: "Por desempenho: o layout deixaria a página de erro lenta."
],
correta: :b,
explicacao:
"A página de erro precisa funcionar justamente quando algo deu errado. Quanto menos ela depender do resto da aplicação, menor a chance de falhar também. Pelo mesmo motivo a configuração usa `render_errors: [..., layout: false]`, e o root layout também não é aplicado."
},
formato_sem_rota: %{
texto: "chega `GET /api/nada`, sem cabeçalho `Accept`. Não existe rota para esse caminho. Em que formato sai a página de erro?",
opcoes: [
a: "JSON, porque o caminho começa com `/api`.",
b: "HTML: nenhuma pipeline rodou, e sem `Accept` nem `_format` vale o primeiro formato de `render_errors`.",
c: "Texto puro, porque o formato é desconhecido.",
d: "O formato do último controller que respondeu."
],
correta: :b,
explicacao:
"O `plug :accepts, [\"json\"]` da pipeline `:api` só roda depois que uma rota casa. Sem rota, o formato é negociado na hora de renderizar o erro, a partir do cabeçalho `Accept` ou do parâmetro `_format`. Se a requisição não diz nada, vale o primeiro formato da lista `render_errors: [formats: [html: ..., json: ...]]`, que é HTML."
},
status_padrao: %{
texto: "a action levanta `HelloWeb.SomethingNotFoundError`, definida só com `defexception [:message]`. Com que status sai a resposta?",
opcoes: [
a: "404, porque o nome da exceção termina em `NotFoundError`.",
b: "500.",
c: "400.",
d: "Nenhum: a conexão é encerrada sem resposta."
],
correta: :b,
explicacao:
"Por padrão, Plug e Phoenix tratam toda exceção como erro 500. O nome não importa. Para outro status, a exceção precisa implementar o protocolo `Plug.Exception` ou ter um campo `plug_status`."
},
acoes_producao: %{
texto: "a exceção agora tem uma ação, \"Run seeds\". Quem acessa a aplicação em produção, com `debug_errors: false`, vê esse botão?",
opcoes: [
a: "Sim, em qualquer ambiente.",
b: "Sim, mas só quem estiver logado.",
c: "Não. As ações só aparecem na página de depuração, quando `debug_errors` é `true`.",
d: "Não, porque o botão só aparece em respostas JSON."
],
correta: :c,
explicacao:
"As ações são desenhadas pelo `Plug.Debugger`, que só existe no endpoint compilado com `debug_errors: true`. Em produção quem responde é a `ErrorHTML`, que recebe o status e ignora as ações. O handler executa código na máquina do servidor, e é por isso que ele fica restrito ao desenvolvimento."
}
}
end
# --- soluções --------------------------------------------------------------
@doc "Um botão que revela a solução de um exercício."
def solucao(id) do
quadro = Kino.Frame.new()
botao = Kino.Control.button("Mostrar a solução")
Kino.listen(botao, fn _evento ->
Kino.Frame.render(quadro, Kino.Markdown.new(cerca(Map.fetch!(solucoes(), id), "elixir")))
end)
Kino.Layout.grid([botao, quadro])
end
defp solucoes do
%{
producao: ~S'''
# Na célula config/dev.exs:
Trilha.config(debug_errors: false)
# Depois avalie a célula do endpoint.
''',
clausula: ~S'''
defmodule HelloWeb.ErrorHTML do
use HelloWeb, :html
# embed_templates "error_html/*", root: Trilha.pasta()
# A cláusula específica vem antes da genérica.
def render("404.html", _assigns) do
"Page Not Found"
end
def render(template, _assigns) do
Phoenix.Controller.status_message_from_template(template)
end
end
''',
template: ~S'''
defmodule HelloWeb.ErrorHTML do
use HelloWeb, :html
embed_templates "error_html/*", root: Trilha.pasta()
def render(template, _assigns) do
Phoenix.Controller.status_message_from_template(template)
end
end
''',
json: ~S'''
defmodule HelloWeb.ErrorJSON do
def render("404.json", _assigns) do
%{errors: %{detail: "Recurso não encontrado"}}
end
def render(template, _assigns) do
%{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
end
end
''',
status_404: ~S'''
# Jeito 1: implementar o protocolo.
defimpl Plug.Exception, for: HelloWeb.SomethingNotFoundError do
def status(_exception), do: 404
def actions(_exception), do: []
end
# Jeito 2: na célula da exceção, um campo plug_status.
defmodule HelloWeb.SomethingNotFoundError do
defexception [:message, plug_status: 404]
end
''',
acionavel: ~S'''
defimpl Plug.Exception, for: HelloWeb.SomethingNotFoundError do
def status(_exception), do: 404
def actions(_exception) do
[
%{
label: "Run seeds",
handler: {Hello.Messages, :seed, []}
}
]
end
end
''',
vendido: ~S'''
# No router, dentro do scope "/":
get "/produtos/:id", ProdutoController, :show
defmodule HelloWeb.ProdutoVendidoError do
defexception [:message, plug_status: 410]
end
defmodule HelloWeb.ProdutoController do
use HelloWeb, :controller
def show(conn, %{"id" => id}) do
case Hello.Catalogo.buscar(id) do
nil -> raise HelloWeb.SomethingNotFoundError, "no product with id #{id}"
%{vendido: true} = produto -> raise HelloWeb.ProdutoVendidoError, "#{produto.nome} was sold"
produto -> render(conn, :show, produto: produto)
end
end
end
defmodule HelloWeb.ProdutoHTML do
use HelloWeb, :html
def show(assigns) do
~H"""
<Layouts.app flash={@flash}>
<h2>{@produto.nome}</h2>
</Layouts.app>
"""
end
end
# O template, com Trilha.arquivo/2:
Trilha.arquivo("error_html/410.html.heex", ~S"""
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="utf-8"/>
<title>Item vendido</title>
</head>
<body>
<p>Este item já foi vendido.</p>
<p><a href="/">Voltar à vitrine</a></p>
</body>
</html>
""")
# Por fim, avalie de novo a célula do HelloWeb.ErrorHTML.
'''
}
end
# --- conferência dos exercícios ----------------------------------------------
@doc "Confere um exercício fazendo requisições de verdade à aplicação."
def conferir(id) do
resultado =
Enum.reduce_while(Map.fetch!(verificacoes(), id), [], fn {descricao, teste}, feitos ->
case executar(teste) do
:ok -> {:cont, ["- Passou: " <> descricao | feitos]}
{:erro, dica} -> {:halt, ["- **Ainda não:** " <> descricao <> "\n\n " <> dica | feitos]}
end
end)
Kino.Markdown.new(resultado |> Enum.reverse() |> Enum.join("\n"))
end
defp executar(teste) do
teste.()
rescue
erro -> {:erro, "O teste levantou uma exceção: `" <> Exception.format_banner(:error, erro) <> "`"}
end
@dica_depuracao "A resposta veio da página de depuração: o endpoint ainda está compilado com `debug_errors: true`. Na célula `config/dev.exs`, use `Trilha.config(debug_errors: false)`, avalie-a e avalie a célula do endpoint."
defp verificacoes do
%{
producao: [
{"`GET /such/a/wrong/path` não passa mais pela página de depuração.",
fn -> if depuracao?(resposta(:get, "/such/a/wrong/path")), do: {:erro, @dica_depuracao}, else: :ok end},
{"A resposta tem status 404.", fn -> status("/such/a/wrong/path", 404) end},
{"O corpo é exatamente `Not Found`.", fn -> corpo_igual("/such/a/wrong/path", "Not Found") end}
],
clausula: [
{"O endpoint está sem a página de depuração.", fn -> sem_depuracao() end},
{"`GET /such/a/wrong/path` responde 404 com o corpo `Page Not Found`.",
fn ->
case corpo_igual("/such/a/wrong/path", "Page Not Found") do
:ok -> :ok
{:erro, dica} -> {:erro, dica <> " A cláusula de `\"404.html\"` precisa vir antes da cláusula genérica, que casa com qualquer template."}
end
end},
{"O template 500 continua com a mensagem padrão.",
fn -> igual(Phoenix.Template.render_to_string(HelloWeb.ErrorHTML, "500", "html", %{}), "Internal Server Error") end}
],
template: [
{"O endpoint está sem a página de depuração.", fn -> sem_depuracao() end},
{"`HelloWeb.ErrorHTML` tem a função `404/1`, gerada a partir de `404.html.heex`.",
fn ->
if function_exported?(HelloWeb.ErrorHTML, :"404", 1),
do: :ok,
else: {:erro, "A função não existe. Grave o arquivo (célula do template), descomente o `embed_templates` e avalie a célula do `ErrorHTML`."}
end},
{"A cláusula `render(\"404.html\", _)` foi removida.",
fn ->
case HelloWeb.ErrorHTML.render("404.html", %{}) do
"Not Found" -> :ok
outro -> {:erro, "`render(\"404.html\", %{})` ainda devolve `#{inspect(outro)}`. Remova a cláusula: com o template embutido, ela nunca é usada."}
end
end},
{"`GET /such/a/wrong/path` responde 404 com a página do template.",
fn -> corpo_contem("/such/a/wrong/path", "Sorry, the page you are looking for does not exist.") end},
{"O template não passou pelo layout: a página tem um único `<html>`.",
fn ->
r = resposta(:get, "/such/a/wrong/path")
n = length(String.split(r.corpo || "", "<html")) - 1
if n == 1, do: :ok, else: {:erro, "Achei #{n} tags `<html>` no corpo."}
end}
],
json: [
{"`GET /such/a/wrong/path` com `Accept: application/json` responde 404 em JSON.",
fn ->
r = resposta(:get, "/such/a/wrong/path", accept: "application/json")
cond do
depuracao?(r) -> {:erro, @dica_depuracao}
r.status != 404 -> {:erro, "O status foi `#{inspect(r.status)}`."}
not String.contains?(r.content_type || "", "json") -> {:erro, "O content-type foi `#{r.content_type}`."}
true -> :ok
end
end},
{"O corpo é `{\"errors\":{\"detail\":\"Recurso não encontrado\"}}`.",
fn ->
r = resposta(:get, "/such/a/wrong/path", accept: "application/json")
case Jason.decode(r.corpo || "") do
{:ok, %{"errors" => %{"detail" => "Recurso não encontrado"}}} -> :ok
{:ok, outro} -> {:erro, "O JSON recebido foi `#{Jason.encode!(outro)}`."}
_ -> {:erro, "O corpo não é JSON."}
end
end},
{"O erro 500 em JSON continua com a mensagem padrão.",
fn -> igual(HelloWeb.ErrorJSON.render("500.json", %{}), %{errors: %{detail: "Internal Server Error"}}) end}
],
status_404: [
{"`Plug.Exception.status/1` devolve 404 para `HelloWeb.SomethingNotFoundError`.",
fn -> igual(Plug.Exception.status(struct(HelloWeb.SomethingNotFoundError, message: "x")), 404) end},
{"`GET /messages/99` responde 404.", fn -> status("/messages/99", 404) end},
{"`GET /api/messages/99` responde 404 em JSON.",
fn ->
r = resposta(:get, "/api/messages/99")
cond do
r.status != 404 -> {:erro, "O status foi `#{inspect(r.status)}`."}
not String.contains?(r.content_type || "", "json") -> {:erro, "O content-type foi `#{r.content_type}`."}
true -> :ok
end
end},
{"`GET /messages/1` continua respondendo 200.", fn -> status("/messages/1", 200) end}
],
acionavel: [
{"`Plug.Exception.actions/1` devolve a ação `Run seeds` com o handler `{Hello.Messages, :seed, []}`.",
fn ->
acoes = Plug.Exception.actions(struct(HelloWeb.SomethingNotFoundError, message: "x"))
if Enum.any?(acoes, &match?(%{label: "Run seeds", handler: {Hello.Messages, :seed, []}}, &1)),
do: :ok,
else: {:erro, "`actions/1` devolveu `#{inspect(acoes)}`."}
end},
{"Com as mensagens apagadas, a página de depuração de `GET /messages` mostra o botão `Run seeds`.",
fn ->
Hello.Messages.clear()
r = resposta(:get, "/messages", accept: "text/html")
cond do
not depuracao?(r) ->
{:erro, "A resposta veio da `ErrorHTML`, não da página de depuração. Use `Trilha.config(debug_errors: true)` na célula `config/dev.exs` e avalie a célula do endpoint."}
String.contains?(r.corpo, "Run seeds") ->
:ok
true ->
{:erro, "A página de depuração não tem o botão. A exceção levantada foi `#{inspect(r.excecao)}`."}
end
end},
{"Clicar no botão executa o handler e redireciona de volta para `/messages`.",
fn ->
Hello.Messages.clear()
pagina = resposta(:get, "/messages", accept: "text/html")
case Regex.run(~r/name="encoded_handler" value="([^"]+)"/, pagina.corpo || "") do
[_, handler] ->
clique =
resposta(:post, "/__plug__/debugger/action",
params: %{"encoded_handler" => handler, "last_path" => "/messages"}
)
cond do
clique.status != 302 -> {:erro, "O clique respondeu com status `#{inspect(clique.status)}`."}
Hello.Messages.list() == [] -> {:erro, "O handler rodou, mas as mensagens continuam vazias."}
true -> :ok
end
nil ->
{:erro, "Não achei o formulário do botão na página de depuração."}
end
end},
{"Depois do clique, `GET /messages` lista as mensagens.", fn -> corpo_contem("/messages", "Oi, Ana", false) end}
],
vendido: [
{"`HelloWeb.ProdutoVendidoError` tem status 410.",
fn -> igual(Plug.Exception.status(struct(HelloWeb.ProdutoVendidoError, message: "x")), 410) end},
{"`GET /produtos/1` responde 200 e mostra `Jaqueta jeans`.",
fn ->
with :ok <- status("/produtos/1", 200), do: corpo_contem("/produtos/1", "Jaqueta jeans", false)
end},
{"`GET /produtos/2`, item vendido, responde 410.", fn -> status("/produtos/2", 410) end},
{"A página do 410 diz `Este item já foi vendido`.", fn -> corpo_contem("/produtos/2", "Este item já foi vendido") end},
{"`GET /produtos/99` responde 404.", fn -> status("/produtos/99", 404) end}
]
}
end
defp sem_depuracao do
if depuracao?(resposta(:get, "/such/a/wrong/path")), do: {:erro, @dica_depuracao}, else: :ok
end
defp igual(valor, esperado) do
if valor == esperado, do: :ok, else: {:erro, "Esperado `#{inspect(esperado)}`, veio `#{inspect(valor)}`."}
end
defp status(caminho, esperado) do
r = resposta(:get, caminho)
if r.status == esperado do
:ok
else
excecao = if r.excecao, do: " A requisição levantou `" <> Exception.format_banner(:error, r.excecao) <> "`.", else: ""
{:erro, "`GET #{caminho}` respondeu com status `#{inspect(r.status)}`." <> excecao}
end
end
defp corpo_igual(caminho, esperado) do
r = resposta(:get, caminho)
cond do
depuracao?(r) -> {:erro, @dica_depuracao}
r.corpo == esperado -> :ok
true -> {:erro, "O corpo de `GET #{caminho}` foi `#{inicio(r.corpo)}`."}
end
end
defp corpo_contem(caminho, texto, exigir_sem_depuracao \\ true) do
r = resposta(:get, caminho)
cond do
exigir_sem_depuracao and depuracao?(r) -> {:erro, @dica_depuracao}
String.contains?(r.corpo || "", texto) -> :ok
true -> {:erro, "A resposta de `GET #{caminho}` (status `#{inspect(r.status)}`) não contém esse texto. Ela começa com `#{inicio(r.corpo)}`."}
end
end
defp inicio(nil), do: "(vazio)"
defp inicio(corpo), do: corpo |> String.slice(0, 120) |> String.replace("`", "'") |> String.replace("\n", " ")
end
# Começa sem templates gravados por execuções anteriores.
File.rm_rf!(Trilha.pasta())
File.mkdir_p!(Path.join(Trilha.pasta(), "error_html"))
Como usar este notebook
Este notebook acompanha o guia Custom Error Pages da documentação do Phoenix 1.8, na mesma ordem e com o link de cada seção. Ele parte da aplicação Hello no ponto em que o notebook 01_ciclo_de_vida_da_requisicao.livemd terminou: a página inicial, /hello e /hello/:messenger. Para este guia, a aplicação ganha também uma lista de mensagens, que serve de exemplo de recurso que pode não ser encontrado.
As convenções são as mesmas dos notebooks anteriores: uma célula equivale a um arquivo, as perguntas "Antes de continuar" vêm antes da explicação, e os erros fazem parte do conteúdo, mais ainda neste guia. A aplicação fica em http://localhost:4010. Se outro notebook estiver com o runtime ligado, desconecte-o antes, porque todos usam a mesma porta.
Duas novidades:
- Uma célula de configuração. A célula
config/dev.exsmuda a configuração do endpoint, como você faria no arquivo de mesmo nome do projeto. Depois de alterá-la, avalie a célula do endpoint, logo abaixo dela. - Templates em arquivos. A página de erro do guia é um arquivo
.heex. O notebook grava esse arquivo de verdade, numa pasta temporária que faz o papel delib/hello_web/controllers, e a view o embute comembed_templates, como no projeto.
Trilha.mostrar/3 faz uma requisição e mostra a resposta que foi enviada: status, content-type e corpo, além da exceção, quando houver, e de quem escreveu a resposta. Neste guia isso importa, porque quase todas as requisições terminam em exceção e ainda assim enviam uma resposta.
Avalie a primeira célula, no topo, antes de tudo.
A aplicação Hello dentro do notebook
As células desta seção definem a aplicação. Avalie todas, na ordem. A ordem importa: o router e o endpoint vêm antes das views e dos controllers porque o sigil ~p, que aparece em um dos templates, consulta o router quando o módulo é compilado e o endpoint quando a página é renderizada.
lib/hello_web.ex: além do que os notebooks anteriores tinham, define verified_routes, que habilita o ~p nos controllers e nos templates.
defmodule HelloWeb do
def router do
quote do
use Phoenix.Router, helpers: false
import Plug.Conn
import Phoenix.Controller
import Phoenix.LiveView.Router
end
end
def controller do
quote do
use Phoenix.Controller, formats: [:html, :json]
import Plug.Conn
unquote(verified_routes())
end
end
def html do
quote do
use Phoenix.Component
alias HelloWeb.Layouts
unquote(verified_routes())
end
end
def verified_routes do
quote do
use Phoenix.VerifiedRoutes,
endpoint: HelloWeb.Endpoint,
router: HelloWeb.Router,
statics: ~w(assets fonts images favicon.ico robots.txt)
end
end
defmacro __using__(which) when is_atom(which) do
apply(__MODULE__, which, [])
end
end
lib/hello_web/router.ex: as rotas do notebook 01, mais as de mensagens. /api/messages/:id usa a pipeline :api e responde JSON.
defmodule HelloWeb.Router do
use HelloWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {HelloWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", HelloWeb do
pipe_through :browser
get "/", PageController, :home
get "/hello", HelloController, :index
get "/hello/:messenger", HelloController, :show
get "/messages", MessageController, :index
get "/messages/:id", MessageController, :show
end
scope "/api", HelloWeb do
pipe_through :api
get "/messages/:id", MessageController, :show
end
end
Avaliar o router agora mostra avisos de que os controllers ainda não existem (init/1 is undefined). É esperado: eles vêm logo abaixo.
config/dev.exs: o trecho de configuração que este guia altera. Em desenvolvimento, debug_errors vem ligado.
Trilha.config(debug_errors: true)
lib/hello_web/endpoint.ex
defmodule HelloWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :hello
@session_options [
store: :cookie,
key: "_hello_key",
signing_salt: "trilha00",
same_site: "Lax"
]
plug Plug.RequestId
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Phoenix.json_library()
plug Plug.MethodOverride
plug Plug.Head
plug Plug.Session, @session_options
plug HelloWeb.Router
end
lib/hello_web/components/layouts.ex
defmodule HelloWeb.Layouts do
use Phoenix.Component
def root(assigns) do
~H"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Hello</title>
</head>
<body>
{@inner_content}
</body>
</html>
"""
end
attr :flash, :map, default: %{}
slot :inner_block, required: true
def app(assigns) do
~H"""
<header>
<strong>Hello</strong> · layout da aplicação
</header>
<main>
{render_slot(@inner_block)}
</main>
"""
end
end
lib/hello_web/controllers/error_html.ex: como o mix phx.new gera na versão 1.8. A única diferença está na linha comentada do embed_templates: no projeto, ela procura a pasta error_html ao lado do próprio arquivo; aqui, a opção root: aponta para a pasta temporária do notebook.
defmodule HelloWeb.ErrorHTML do
use HelloWeb, :html
# If you want to customize your error pages,
# uncomment the embed_templates/1 call below
# and add pages to the error directory:
#
# * lib/hello_web/controllers/error_html/404.html.heex
# * lib/hello_web/controllers/error_html/500.html.heex
#
# embed_templates "error_html/*", root: Trilha.pasta()
# The default is to render a plain text page based on
# the template name. For example, "404.html" becomes
# "Not Found".
def render(template, _assigns) do
Phoenix.Controller.status_message_from_template(template)
end
end
lib/hello_web/controllers/error_json.ex
defmodule HelloWeb.ErrorJSON do
# If you want to customize a particular status code,
# you may add your own clauses, such as:
#
# def render("500.json", _assigns) do
# %{errors: %{detail: "Internal Server Error"}}
# end
# By default, Phoenix returns the status message from
# the template name. For example, "404.json" becomes
# "Not Found".
def render(template, _assigns) do
%{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
end
end
lib/hello_web/controllers/: a página inicial e as páginas do notebook 01.
defmodule HelloWeb.PageController do
use HelloWeb, :controller
def home(conn, _params) do
render(conn, :home)
end
end
defmodule HelloWeb.PageHTML do
use HelloWeb, :html
def home(assigns) do
~H"""
<Layouts.app flash={@flash}>
<h1>Welcome to Phoenix!</h1>
</Layouts.app>
"""
end
end
defmodule HelloWeb.HelloController do
use HelloWeb, :controller
def index(conn, _params) do
render(conn, :index)
end
def show(conn, %{"messenger" => messenger}) do
render(conn, :show, messenger: messenger)
end
end
defmodule HelloWeb.HelloHTML do
use HelloWeb, :html
def index(assigns) do
~H"""
<section>
<h2>Hello World, from Phoenix!</h2>
</section>
"""
end
def show(assigns) do
~H"""
<Layouts.app flash={@flash}>
<section>
<h2>Hello World, from {@messenger}!</h2>
</section>
</Layouts.app>
"""
end
end
lib/hello/messages.ex: as mensagens ficam guardadas em um Agent, um processo que mantém um valor. É só o apoio do exemplo; em um projeto elas viriam do banco de dados. clear/0 apaga todas e seed/0 recria as duas de exemplo.
defmodule Hello.Messages do
use Agent
@seeds %{
"1" => %{id: "1", text: "Oi, Ana"},
"2" => %{id: "2", text: "Oi, Bia"}
}
def start_link(_), do: Agent.start_link(fn -> @seeds end, name: __MODULE__)
def list, do: Agent.get(__MODULE__, fn mensagens -> mensagens |> Map.values() |> Enum.sort_by(& &1.id) end)
def find_message(id), do: Agent.get(__MODULE__, &Map.get(&1, id))
def clear, do: Agent.update(__MODULE__, fn _ -> %{} end)
def seed, do: Agent.update(__MODULE__, fn _ -> @seeds end)
end
lib/hello_web.ex, no fim do arquivo: a exceção do guia, ainda na forma mais simples.
defmodule HelloWeb.SomethingNotFoundError do
defexception [:message]
end
lib/hello_web/controllers/message_controller.ex, message_html.ex e message_json.ex: quando a mensagem não existe, ou não há nenhuma, a action levanta a exceção.
defmodule HelloWeb.MessageController do
use HelloWeb, :controller
def index(conn, _params) do
case Hello.Messages.list() do
[] -> raise HelloWeb.SomethingNotFoundError, "there are no messages yet"
messages -> render(conn, :index, messages: messages)
end
end
def show(conn, %{"id" => id}) do
case Hello.Messages.find_message(id) do
nil -> raise HelloWeb.SomethingNotFoundError, "no message with id #{id}"
message -> render(conn, :show, message: message)
end
end
end
defmodule HelloWeb.MessageHTML do
use HelloWeb, :html
def index(assigns) do
~H"""
<Layouts.app flash={@flash}>
<h2>Messages</h2>
<ul>
<li :for={message <- @messages}>
<a href={~p"/messages/#{message.id}"}>{message.text}</a>
</li>
</ul>
</Layouts.app>
"""
end
def show(assigns) do
~H"""
<Layouts.app flash={@flash}>
<h2>Message {@message.id}</h2>
<p>{@message.text}</p>
</Layouts.app>
"""
end
end
defmodule HelloWeb.MessageJSON do
def show(%{message: message}) do
%{data: %{id: message.id, text: message.text}}
end
end
A célula abaixo inicia as mensagens e o endpoint, o equivalente a mix phx.server.
{:ok, _pid} = Kino.start_child(Hello.Messages)
{:ok, _pid} = Kino.start_child(HelloWeb.Endpoint)
Kino.Markdown.new("Aplicação no ar em <http://localhost:4010>. Abra <http://localhost:4010/messages>: você deve ver as duas mensagens.")
Se a célula falhar com :eaddrinuse, a porta 4010 já está ocupada. Troque o número em http: [..., port: 4010] na primeira célula do notebook e reconecte o runtime.
As views de erro
Na documentação: The Error Views
Projetos novos têm duas views de erro, ErrorHTML e ErrorJSON, em lib/hello_web/controllers/. Elas tratam os erros de um jeito geral para cada formato, a partir de um lugar só. Você já as avaliou na seção anterior. Quem as liga ao endpoint é a configuração render_errors, que está na primeira célula do notebook:
render_errors: [formats: [html: HelloWeb.ErrorHTML, json: HelloWeb.ErrorJSON], layout: false]
Antes de mexer nelas, veja o que acontece hoje com uma rota que não existe:
Trilha.mostrar(:get, "/such/a/wrong/path")
A exceção é Phoenix.Router.NoRouteError, o status é 404, e quem escreveu a resposta não foi a ErrorHTML: foi a página de depuração do Plug.Debugger. Em desenvolvimento, o Phoenix depura os erros por padrão e mostra uma página detalhada, com a exceção, o stacktrace e os dados da requisição. Na versão HTML, para rotas inexistentes, ela lista também as rotas da aplicação.
O corpo veio em Markdown porque a requisição do Trilha não diz que aceita HTML. O navegador diz, e recebe a página completa. Compare:
Trilha.mostrar(:get, "/such/a/wrong/path", accept: "text/html")
Abra também http://localhost:4010/such/a/wrong/path no navegador.
A página de produção
Para ver a página que a aplicação serviria em produção, o guia desliga debug_errors no config/dev.exs.
Trilha.pergunta(:producao)
Sua vez
- Na célula
config/dev.exs, na seção anterior, troquetrueporfalsee avalie a célula. - Avalie a célula do endpoint, logo abaixo dela.
debug_errors é lido quando o módulo do endpoint é compilado: é nesse momento que use Phoenix.Endpoint decide se inclui o Plug.Debugger. Por isso basta recompilar o endpoint; o servidor que está no ar passa a usar a versão nova na requisição seguinte. No projeto, o guia manda reiniciar o servidor, e o efeito é o mesmo: a configuração é lida de novo e o endpoint é recompilado.
Trilha.mostrar(:get, "/such/a/wrong/path")
Trilha.conferir(:producao)
Trilha.solucao(:producao)
Não é uma página interessante: só a string Not Found, sem marcação nem estilo.
De onde vem o Not Found
Trilha.pergunta(:fallback)
A resposta está na ErrorHTML:
def render(template, _assigns) do
Phoenix.Controller.status_message_from_template(template)
end
render/2 recebe um template e um mapa de assigns, que aqui é ignorado. Quando um controller chama render(conn, :some_template), o Phoenix procura primeiro uma função some_template/1 no módulo da view. Se ela não existe, recorre a render/2, passando o nome do template e o formato, por exemplo "some_template.html". Para os erros vale a mesma regra: o Phoenix renderiza o template com o nome do status, 404, e a ErrorHTML não tem uma função 404/1.
A célula abaixo renderiza alguns templates da ErrorHTML diretamente, sem requisição. status_message_from_template/1 lê o número no nome do template e devolve a mensagem padrão do HTTP para ele.
for template <- ["404", "500", "403", "401"] do
{template, Phoenix.Template.render_to_string(HelloWeb.ErrorHTML, template, "html", %{})}
end
Para ter uma página de erro própria, então, basta definir uma cláusula de render/2 para o template certo.
Sua vez: uma cláusula para o 404
Na célula da HelloWeb.ErrorHTML, acrescente a cláusula do guia:
def render("404.html", _assigns) do
"Page Not Found"
end
A posição importa. As cláusulas de uma função são testadas de cima para baixo, e a cláusula genérica, render(template, _assigns), casa com qualquer template.
Trilha.conferir(:clausula)
Trilha.solucao(:clausula)
Um template para o 404
O guia vai além da string e cria um template em lib/hello_web/controllers/error_html/404.html.heex, com uma página HTML completa. Estas são as mudanças em relação ao template do guia: foram retiradas as linhas que carregam CSS, JavaScript e a imagem do logo, porque a aplicação do notebook não serve arquivos estáticos.
A célula abaixo grava o arquivo. Avalie-a.
Trilha.arquivo("error_html/404.html.heex", ~S"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Welcome to Phoenix!</title>
</head>
<body>
<header>
<section class="container">
<nav>
<ul>
<li><a href="https://phoenix.hexdocs.pm/overview.html">Get Started</a></li>
</ul>
</nav>
</section>
</header>
<main class="container">
<section class="phx-hero">
<p>Sorry, the page you are looking for does not exist.</p>
</section>
</main>
</body>
</html>
""")
O arquivo sozinho não muda nada. É preciso dizer ao Phoenix para embutir os templates da pasta no módulo, com embed_templates. Na compilação, cada arquivo vira uma função com o nome dele: 404.html.heex vira 404/1.
Trilha.pergunta(:precedencia)
Sua vez: embutir o template
Na célula da HelloWeb.ErrorHTML:
- descomente a linha
embed_templates "error_html/*", root: Trilha.pasta(); - remova a cláusula
render("404.html", _assigns)que você acabou de escrever.
Trilha.conferir(:template)
Abra http://localhost:4010/such/a/wrong/path no navegador: a página nova aparece no lugar da string.
Trilha.solucao(:template)
Para conferir a pergunta acima por conta própria, devolva a cláusula render("404.html", _assigns) à ErrorHTML mantendo o embed_templates, avalie a célula e rode Trilha.mostrar(:get, "/such/a/wrong/path"). A página continua sendo a do template. Depois remova a cláusula de novo.
Experimente: os assigns de um template de erro
O template de erro recebe assigns, como qualquer template. Entre eles estão @status, o status da resposta, e @reason, a exceção. Na célula que grava o arquivo, acrescente <p>Status: {@status}</p> logo depois do parágrafo Sorry..., avalie essa célula e depois a da ErrorHTML (o embed_templates lê os arquivos na compilação), e rode:
Trilha.mostrar(:get, "/such/a/wrong/path")
Mostrar @reason para o usuário não é uma boa ideia em produção: a mensagem da exceção pode expor detalhes internos da aplicação.
Sem o layout da aplicação
Trilha.pergunta(:layout)
O template 404 não foi renderizado pelo layout da aplicação, embora a página de erro devesse ter a cara do resto do site. É de propósito: evita erros em cascata. Se a aplicação falhou por causa de um erro no layout, tentar renderizar o layout de novo só dispara outro erro. O ideal é reduzir ao mínimo as dependências e a lógica dos templates de erro e compartilhar só o necessário.
Erros em JSON
A ErrorJSON funciona do mesmo jeito, mas render/2 devolve um mapa, que o Phoenix converte em JSON. Qual das duas views responde depende do formato da requisição.
Trilha.mostrar(:get, "/such/a/wrong/path", accept: "application/json")
O parâmetro _format tem o mesmo efeito do cabeçalho:
Trilha.mostrar(:get, "/such/a/wrong/path?_format=json")
Trilha.pergunta(:formato_sem_rota)
Trilha.mostrar(:get, "/api/nada")
O caminho começa com /api, mas nenhuma rota casou e a pipeline :api não rodou. Quando uma rota casa, o formato já foi negociado pela pipeline, e o erro sai nele. Você vai ver isso na seção seguinte, com /api/messages/99.
Sua vez: o 404 em JSON
Na célula da HelloWeb.ErrorJSON, faça o template 404.json devolver %{errors: %{detail: "Recurso não encontrado"}}. Os outros status continuam com a mensagem padrão. O comentário no topo da célula mostra a forma da cláusula.
Trilha.conferir(:json)
Trilha.solucao(:json)
Exceções próprias
Na documentação: Custom exceptions
O Elixir tem a macro defexception/1 para definir exceções. Exceções são structs, e structs precisam ser definidas dentro de módulos. Para criar uma exceção, define-se um módulo novo, por convenção com "Error" no nome, e dentro dele defexception/1. O guia sugere o arquivo lib/hello_web.ex. É a célula que você avaliou na seção da aplicação:
defmodule HelloWeb.SomethingNotFoundError do
defexception [:message]
end
Ela é levantada assim: raise HelloWeb.SomethingNotFoundError, "oops". A célula abaixo levanta a exceção e a captura, para mostrar que o valor levantado é uma struct comum.
try do
raise HelloWeb.SomethingNotFoundError, "oops"
rescue
erro -> {erro, Exception.message(erro)}
end
O MessageController levanta essa exceção quando a mensagem pedida não existe.
Trilha.pergunta(:status_padrao)
Trilha.mostrar(:get, "/messages/99")
Plug.Exception.status(%HelloWeb.SomethingNotFoundError{message: "oops"})
Por padrão, Plug e Phoenix tratam todas as exceções como erros 500. O Plug oferece um protocolo, Plug.Exception, com o qual uma exceção define o próprio status e as ações que pode oferecer na página de depuração. É ele que a última célula consultou.
Para que HelloWeb.SomethingNotFoundError responda com 404, há dois caminhos. O primeiro é implementar o protocolo, também em lib/hello_web.ex:
defimpl Plug.Exception, for: HelloWeb.SomethingNotFoundError do
def status(_exception), do: 404
def actions(_exception), do: []
end
O segundo é definir um campo plug_status na própria struct da exceção:
defmodule HelloWeb.SomethingNotFoundError do
defexception [:message, plug_status: 404]
end
Phoenix.Router.NoRouteError, a exceção das rotas inexistentes, usa o segundo caminho. Implementar o protocolo à mão é útil em algumas situações, como para oferecer erros acionáveis, assunto da próxima seção.
Sua vez: um 404 para a exceção
Implemente o protocolo na célula abaixo (o primeiro caminho) ou acrescente o campo plug_status à célula da exceção, na seção da aplicação (o segundo). Em seguida, confira.
# lib/hello_web.ex: implemente aqui Plug.Exception para HelloWeb.SomethingNotFoundError.
Trilha.conferir(:status_404)
Agora as duas versões de /messages/99. A resposta é a mesma página 404 da seção anterior, porque é a mesma view de erro renderizando o template 404. Na rota da API, o formato já tinha sido negociado pela pipeline :api, e quem responde é a ErrorJSON.
Trilha.mostrar(:get, "/messages/99")
Trilha.mostrar(:get, "/api/messages/99")
Trilha.solucao(:status_404)
Erros acionáveis
Na documentação: Actionable errors
Ações de exceção são funções que podem ser disparadas a partir da página de erro. Elas são uma lista de mapas, cada um com um label e um handler a executar. O Phoenix, por exemplo, mostra um erro quando há migrations pendentes e oferece na página um botão que as executa.
Quando debug_errors é true, as ações aparecem na página de erro como uma coleção de botões, e seguem este formato:
[
%{
label: String.t(),
handler: {module(), function :: atom(), args :: []}
}
]
O handler é uma tupla de módulo, função e argumentos, e não uma função anônima, porque ele vai e volta pelo navegador: o botão é um formulário que envia o handler assinado com o secret_key_base do endpoint, e o Plug.Debugger só o executa se a assinatura conferir.
No guia, a ação roda o arquivo de seeds ({Code, :eval_file, ["priv/repo/seeds.exs"]}). O notebook não tem banco nem arquivo de seeds, e o equivalente é Hello.Messages.seed/0, que recria as mensagens de exemplo.
Trilha.pergunta(:acoes_producao)
Sua vez: o botão "Run seeds"
- Na célula
config/dev.exs, volte paraTrilha.config(debug_errors: true), avalie-a e avalie a célula do endpoint. Sem oPlug.Debuggernão há botões. - Na célula do
defimplda seção anterior, troqueactions/1para devolver a ação abaixo. Se você usou o campoplug_status, escreva agora odefimplcompleto, comstatus/1devolvendo 404.
def actions(_exception) do
[
%{
label: "Run seeds",
handler: {Hello.Messages, :seed, []}
}
]
end
- Apague as mensagens com a célula abaixo. Sem mensagens,
GET /messageslevanta a exceção.
Hello.Messages.clear()
- Abra http://localhost:4010/messages no navegador. A página de depuração mostra o botão Run seeds. Clique nele: o
Plug.DebuggerexecutaHello.Messages.seed()e redireciona de volta para/messages, que agora lista as mensagens.
A conferência faz o mesmo caminho sem o navegador: apaga as mensagens, pede /messages aceitando HTML, encontra o formulário do botão na página e o envia.
Trilha.conferir(:acionavel)
Trilha.solucao(:acionavel)
A versão em Markdown da página de depuração, a que o Trilha recebe sem pedir HTML, não tem os botões. Confira:
Hello.Messages.clear()
Trilha.mostrar(:get, "/messages")
Hello.Messages.seed()
Sem roteiro: o item vendido
O Brechó, o mini-marketplace da trilha, precisa de uma página para itens que já foram vendidos. Uma página 404 estaria errada: o item existiu, só não está mais disponível. Para isso o HTTP tem o status 410, Gone.
A célula abaixo é o catálogo de exemplo. Avalie-a.
defmodule Hello.Catalogo do
@produtos %{
"1" => %{id: "1", nome: "Jaqueta jeans", vendido: false},
"2" => %{id: "2", nome: "Vitrola", vendido: true}
}
def buscar(id), do: Map.get(@produtos, id)
end
Faça, sem roteiro:
- uma rota
GET /produtos/:id, umHelloWeb.ProdutoControllere uma view que mostra o nome do produto; - uma exceção
HelloWeb.ProdutoVendidoErrorcom status 410, levantada pela action quando o produto está vendido; - para um id que não existe, a action levanta
HelloWeb.SomethingNotFoundError, que já responde 404; - um template
error_html/410.html.heex, gravado comTrilha.arquivo/2, com o textoEste item já foi vendido.
A conferência olha o corpo das páginas de erro, então deixe debug_errors em false antes de conferir.
# lib/hello_web/controllers/produto_controller.ex, produto_html.ex e o template 410
Trilha.conferir(:vendido)
Trilha.solucao(:vendido)
No seu projeto
Repita o guia em um projeto gerado por mix phx.new hello. A correspondência é esta:
| Neste notebook | No projeto |
|---|---|
Trilha.config(debug_errors: false) |
debug_errors: false em config/dev.exs e reiniciar o servidor |
Trilha.arquivo("error_html/404.html.heex", ...) |
Criar lib/hello_web/controllers/error_html/404.html.heex |
embed_templates "error_html/*", root: ... |
embed_templates "error_html/*" |
Trilha.mostrar(:get, caminho, accept: ...) |
curl -i -H "accept: application/json" http://localhost:4000/... |
Célula da exceção e do defimpl |
Fim de lib/hello_web.ex |
Roteiro:
- Com
debug_errors: false, acesse http://localhost:4000/such/a/wrong/path e veja oNot Found. - Crie a pasta
error_html, o template404.html.heex, e descomente oembed_templatesemlib/hello_web/controllers/error_html.ex. Recarregue a página. - Rode
curl -i http://localhost:4000/such/a/wrong/pathcom e sem o cabeçalhoaccept: application/jsone compare o content-type das respostas. - Defina
HelloWeb.SomethingNotFoundError, levante-a em uma action e observe no log do servidor o status com que a resposta sai, antes e depois dodefimpl Plug.Exception. - Volte
debug_errorsparatrueantes de continuar a trabalhar. A página de depuração é a melhor ferramenta para entender um erro em desenvolvimento.
O projeto gerado tem uma pasta priv/repo com seeds.exs, e a ação do guia ({Code, :eval_file, ["priv/repo/seeds.exs"]}) funciona como está escrita.
Revisão
Escreva em uma célula de texto, com as suas palavras: o que acontece entre uma action levantar uma exceção e o navegador receber a página de erro, em desenvolvimento e em produção. Depois compare com os pontos abaixo.
- Com
debug_errors: true, oPlug.Debuggerresponde com a página de depuração, em HTML para quem aceita HTML e em Markdown para os demais. É a configuração de desenvolvimento. - Com
debug_errors: false, quem responde é a view de erro configurada emrender_errorspara o formato da requisição:ErrorHTMLouErrorJSON. - O nome do template de erro é o status:
404,500. O Phoenix procura primeiro a função do template (404/1) e depoisrender/2com"404.html". embed_templatestransforma cada arquivo.heexda pasta em uma função com o nome do arquivo. Um template embutido tem precedência sobre uma cláusula derender/2.- Páginas de erro não usam o layout, para que um erro no layout não impeça a página de erro de aparecer.
- Sem rota, nenhuma pipeline roda, e o formato do erro vem do cabeçalho
Acceptou de_format. Com rota, ele vem da negociação feita pela pipeline. - Toda exceção vira 500, a menos que implemente
Plug.Exceptionou tenha o campoplug_status. - As ações de
Plug.Exception.actions/1aparecem como botões só na página de depuração. O handler é uma tupla de módulo, função e argumentos.
Próximo notebook: 06_componentes_e_heex.livemd, sobre function components, HEEx e layouts.