Aprenda / Ruby on Rails

IntermediárioAula 15 de 2016 min0% da trilha

Envelope de resposta e tratamento de erros

Cada endpoint devolvendo um formato de erro diferente obriga o front a tratar cada um na mão — um envelope padronizado e um rescue_from central resolvem isso de uma vez.

Sem um padrão combinado, é fácil um endpoint devolver { error: "..." }, outro devolver { errors: [...] }, e um terceiro simplesmente estourar um 500 cru quando algo falha. O cliente da API — front-end, app mobile, outro serviço — precisa tratar cada formato na mão. Um envelope de resposta combinado com rescue_from centralizado resolve isso de vez, num lugar só.

O formato — sucesso e erro seguem sempre a mesma forma

resposta de sucesso
{
  "data": { "id": "1", "type": "pedido", "attributes": { "total": "150.0" } },
  "meta": { "pagina": 1 }
}
resposta de erro
{
  "error": { "codigo": "nao_encontrado", "mensagem": "Pedido não encontrado" }
}

Um cliente que já sabe ler esse formato consegue tratar qualquer endpoint da API do mesmo jeito — olha se veio data ou error, sem precisar de um tratamento específico por rota.

rescue_from centralizado — um lugar só pra cada tipo de exceção

app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  rescue_from ActiveRecord::RecordNotFound, with: :registro_nao_encontrado
  rescue_from ActiveRecord::RecordInvalid, with: :registro_invalido
  rescue_from CanCan::AccessDenied, with: :acesso_negado

  private

  def registro_nao_encontrado(excecao)
    render json: { error: { codigo: "nao_encontrado", mensagem: excecao.message } }, status: :not_found
  end

  def registro_invalido(excecao)
    render json: { error: { codigo: "invalido", mensagens: excecao.record.errors.full_messages } },
           status: :unprocessable_entity
  end

  def acesso_negado(_excecao)
    render json: { error: { codigo: "acesso_negado", mensagem: "Você não tem permissão para essa ação" } },
           status: :forbidden
  end
end

Como toda classe controller herda de ApplicationController, essas três regras valem pra qualquer controller do projeto — nenhum deles precisa saber que RecordNotFound existe, só precisa deixar o Rails levantar a exceção naturalmente.

O que isso substitui: begin/rescue repetido em cada action

sem rescue_from — repetido, e fácil de divergir
def show
  begin
    pedido = Pedido.find(params[:id])
    render json: pedido
  rescue ActiveRecord::RecordNotFound
    render json: { error: "não encontrado" }, status: :not_found   # formato diferente do combinado acima!
  end
end

Além de repetir o mesmo begin/rescue em toda action que pode levantar RecordNotFound, nada garante que dois desenvolvedores formatem o erro do mesmo jeito — é exatamente esse tipo de divergência que o envelope padronizado tenta eliminar, e ela volta a existir se cada action trata sua própria exceção.

Exercício

params.require(:pedido) levanta ActionController::ParameterMissing quando a chave pedido não vem na requisição. Adicione um rescue_from em ApplicationController pra essa exceção, seguindo o mesmo formato de envelope de erro usado acima.

Quiz rápido

Qual a vantagem de rescue_from centralizado no ApplicationController em vez de begin/rescue espalhado em cada action?
O que um envelope de resposta padronizado (data/meta/error) resolve para quem consome a API?