Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Padrão no retorno de Erros #97

Closed
filipedeschamps opened this issue Aug 10, 2021 · 10 comments
Closed

Padrão no retorno de Erros #97

filipedeschamps opened this issue Aug 10, 2021 · 10 comments
Labels
back Envolve modificações no backend

Comments

@filipedeschamps
Copy link
Owner

filipedeschamps commented Aug 10, 2021

Descrição

Como desenvolvedor do sistema ou consumidor da API, eu gostaria de contar com um padrão de erros, tanto na implementação de erros customizados, quanto no retorno da API.

Motivo

Consultas com sucesso vão acabar não tendo um padrão no seu retorno, pois hora um endpoint pode retornar um objeto, hora pode retornar uma lista, mas retornos com erro podem ter um padrão. Isso é extremamente importante, pois o "caminho feliz" de uma aplicação é geralmente muito discutido, mas o "caminho infeliz" pode fugir do nosso controle e um padrão no objeto de retorno pode ajudar muito a mostrar esse erro de uma forma legal numa interface com o usuário, ou programaticamente quando consultado por um outro programa.

Execução

Tem uma talk que fiz no Pagar.me Talks sobre como fazer erros customizados e de uma forma padronizada, não sei se esse assunto evoluiu ao ponto de mudar alguma coisa, é preciso avaliar.

De qualquer forma, segue aqui uma sugestão da estrutura do objeto de retorno (e sempre super aberto a discussão):

{
    "message": "Mensagem curta sobre o erro.",
    "action": "Uma sugestão, para quem recebeu esse erro, do que fazer para consertar ele.",
    "type": "tipo_do_erro_para_ser_consultado_de_forma_programatica",
    "traceid": "2525ef6e-fa2e-11eb-9a03-0242ac130003",
    "errors": [{
      "field": "username",
      "message": "O nome de usuário deve ser preenchido."
    }, {
      "field": "password",
      "message": "A senha deve ser preenchida."
    }]
}

Dependência

Seria interessante primeiro executar a issue #96 e padronizar os controllers.

@filipedeschamps filipedeschamps added the back Envolve modificações no backend label Aug 10, 2021
@rhandrade
Copy link
Contributor

Show @filipedeschamps... Pelo que entendi identificaríamos os erros pelo status code da requisição. Ai fiquei com uma dúvida.

No exemplo que você deu o erro ocorreu devido a uma regra da aplicação. Nesses casos devemos responder com um status 200, indicando que o servidor processou a requisição e por algum motivo deu erro e incluir uma flag dentro da resposta mostrando que teve um erro (tipo um "success": false), ou usamos os status 4xx para indicar isso, como um 400, por exemplo?

Sempre fico em dúvida nessa parte, porque fico tentado encontrar o melhor status code para descrever o problema retornado. No exemplo que você deu e olhando a definição do http status code, talvez o status 400 não seria a melhor opção, visto que o servidor conseguiu entender a requisição. 🤔

@filipedeschamps
Copy link
Owner Author

Excelente pergunta @rhandrade ! Nós devemos usar os status code sim ao retornar a request no controller 👍 então como você já pode deduzir pelo status code se a request teve sucesso ou não, acho redundante colocar uma propriedade/flag a mais na resposta. Incusive a maioria das bibliotecas que consomem endpoints http vão escolher continuar ou jogar um erro dependendo do status code retornado.

O exemplo que coloquei é meio mock, mas sobre a parte de faltar alguns campos ali, a gente poderia usar o 400 Bad Request que é o mais comum... ou o 422 Unprocessable Entity que não é tão comum, mas é bem específico nessa condição, olha a descrição que legal:

The 422 (Unprocessable Entity) status code means the server
understands the content type of the request entity (hence a
415(Unsupported Media Type) status code is inappropriate), and the
syntax of the request entity is correct (thus a 400 (Bad Request)
status code is inappropriate) but was unable to process the contained
instructions. For example, this error condition may occur if an XML
request body contains well-formed (i.e., syntactically correct), but
semantically erroneous, XML instructions.

@rhandrade
Copy link
Contributor

Show @filipedeschamps, agora consegui entender melhor essa questão. Vlw 👍

@huogerac
Copy link

huogerac commented Aug 19, 2021

Bem legal, ter esta tarefa para padronizar! criar algo inicial para ir modelando o lançamento e tratamento de erros.

Eu gostaria de apresentar aqui uma linha um pouco relacionado com isto, mas é mais abrangente, é o OpenAPI que tambem tem seu estilo de exibir os erros.

Por exemplo,
Vamos dizer que ao tentar obter uma Notícia via ID. E que este ID não existe na base:

  • A camada de serviço pode lançar um DoesntExistError
  • A camada de API captura e transforma em um 404 como uma mensagem amigável.
{
  "detail": "Tabnews ID: 42 not found!",
  "status": 404,
  "title": "Not Found",
  "type": "...."
}

Eu tentei criar uma versão bem inicial do OpenAPI para o tabnews, pensando mais nos status_code e input/output.
Segue abaixo:

## Validate it at: https://apitools.dev/swagger-parser/online/

openapi: 3.0.2

info:
  version: 0.1.0
  title: Tabnews (side project) API Backend
  description: API and database for auth, users and news
  contact:
    name: Tabnews
    email: api@tabnews.com.br
    url: https://tabnews-web.vercel.app/

servers:
  - url: https://tabnewspy.herokuapp.com/api/
    description: Development

paths:
  /api/version:
    get:
      operationId: api.api_version
      summary: Returns the API version
      tags:
        - Status
      responses:
        200:
          description: Success

  /api/auth/login:
    post:
      operationId: api.auth.login
      summary: User login to get a JWT Token
      tags:
        - Auth
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - email
                - password
              properties:
                email:
                  type: string
                  example: john@doe.com
                password:
                  type: string
                  example: "@bC1234"
      responses:
        200:
          description: JWT Access Token
          content:
            application/json:
              schema:
                type: object
                properties:
                  user:
                    $ref: "#/components/schemas/User"
                  token:
                    type: string
                    example: Encripted.JWT.Token
                  refresh_token:
                    type: string
                    example: Another.Encripted.JWT.Token.With.Long.Expiration
        400:
          description: Bad request. You must send an email and password
        401:
          description: Email or password is not valid

  /api/auth/github/login:
    get:
      operationId: api.auth.github_login
      description: Login github
      tags:
        - Auth
      responses:
        302:
          description: Redirect to the authorization

  /api/auth/github/authorize:
    get:
      operationId: api.auth.github_authorize
      description: Token
      tags:
        - Auth
      responses:
        302:
          description: Redirect to the frontend

  /api/tabnews:
    get:
      operationId: api.tabnews.list_tabnews
      summary: Returns the latest tabnews
      tags:
        - Tabnews

      responses:
        200:
          description: The lastest Tabnews
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Tabnews"

    post:
      operationId: api.tabnews.create_tabnews
      summary: Create a new Tabnews
      tags:
        - Tabnews
      security:
        - jwtAuth: [tabnews:create]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - title
              properties:
                title:
                  type: string
                  example: 101 facts that prove the Earth is flat
                  minLength: 12
                description:
                  type: string
                  example: Despite being a maiority, in absolute terms, few people know the real truth...

      responses:
        201:
          description: Tabnews created succesfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Tabnews"
        400:
          description: Invalid data to create a Tabnews
        401:
          description: No Authorization
        403:
          description: No Permission

components:
  securitySchemes:
    jwtAuth:
      type: oauth2
      x-tokenInfoFunc: services.token.check_token_info
      flows:
        authorizationCode:
          tokenUrl: "url"
          authorizationUrl: "url"

  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
          example: 1
        name:
          type: string
          example: John Doe
        email:
          type: string
          example: john@doe.com

    Author:
      type: object
      properties:
        id:
          type: integer
          example: 1
        name:
          type: string
          example: John Doe
        avatar:
          type: string
          example: http://example.com/img/avatar/jose_thumb.png
          nullable: true

    Tabnews:
      type: object
      properties:
        id:
          type: integer
          example: 42
        title:
          type: string
          example: Python is a great option for backend and APIs
        description:
          type: string
          nullable: true
        author:
          $ref: "#/components/schemas/Author"
        created_at:
          type: string
          format: date-time
          example: 2021-01-11T11:32:28Z

p.s:

  • Este YAML pode ser alterado online com este editor: https://editor.swagger.io/
  • Sei que a sugestão é não usar JWT, este YAML está com JWT, mas deve ser uma alteração pequena para usar autenticação via session/cookie
  • Pode notar que as permissões são com base em recursos ao invés de role

Também tentei integrar no Next-JS o OpenAPI, mas falhei miseravelmente:
https://github.com/jellydn/next-swagger-doc 😞

Dai acabei fazendo com uma stack que tenho mais conhecimento, então esta versão rodando no Heroku foi feita com Python/Flask

@rhandrade
Copy link
Contributor

Que maneiro @huogerac. Achei a ideia de usar a OpenAPI sensacional por ela já ser um padrão para descrever APIs bem difundido. Quanto ao exemplo que você deu talvez não seria necessário colocar o status code dentro do objeto, como o @filipedeschamps tinha comentado na minha dúvida...

Esses dias tava fuçando na API beta do OpenAI e curti o modo como eles retornam os erros, deixo um exemplo abaixo. A requisição nesse caso retornou um status code 404 com o seguinte objeto:

{
  "error": {
    "code": null,
    "message": "Engine not found",
    "param": null,
    "type": "invalid_request_error"
  }
}

@filipedeschamps
Copy link
Owner Author

Show pessoal, estou voltando nessa thread :)

Sobre um objeto de erro singular, como é feito no caso onde é preciso retornar múltiplos erros? Por exemplo numa validação de um formulário em que mais de 1 campo pode não passar na validação?

Mas sobre o OpenAPI num geral, ele é uma descrição de como a API funciona, e também define os padrões de resposta, e com isso consegue gerar documentação, correto? To pesquisando a respeito. Quem usa, fala muito bem, mas não to conseguindo achar uma adoção massiva ao ponto de me sentir confortável em colocar essa abstração em cima das respostas. Mas vou estudar mais a respeito 👍

Em paralelo, vejo também que é uma camada que dá para colocar por cima depois, e no estágio atual do projeto, podemos mudar qualquer interface pública que quisermos 🤝

@liverday
Copy link
Contributor

Criei um PR #129 como proposta da padronização dos erros utilizando o onErrorHandler do next-connect. Creio que a sugestão que o @filipedeschamps funciona muito bem, tanto nos casos de erros singulares, como nos erros múltiplos.

@rhandrade
Copy link
Contributor

@filipedeschamps Acredito que quando forem muitos erros poderíamos retornar um array contendo vários objetos erros, cada qual com o erro que aconteceu.

O Laravel tem uma proposta semelhante se não estou enganado, no qual temos um objeto errors, no qual cada propriedade é um campo que causou o erro e o valor é um array contendo com todos os erros de validação para aquele campo.

Sobre a OpenAPI eu entendi isso ai tbm.

@brunofamiliar
Copy link
Contributor

Olá pessoal! Essa issue foi resolvida no PR #131 certo? o que acham de fecharmos para focar nas pendências ainda existentes?

@filipedeschamps
Copy link
Owner Author

Show @rhandrade o custom error consegue esperar um array de erros e retornar no errors um array 👍

@brunofamiliar corretíssimo e valeu pelo toque! Fechando a issue 😍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
back Envolve modificações no backend
Projects
None yet
Development

No branches or pull requests

5 participants