Gestione degli errori


Goa rende possibile descrivere gli errori che i servizi potrebbero ritornare. Da questa descrizione Goa puo generare sia il codice che la documentazione. Il codice fornisce sia la logica di serializzazione che di deserializzazione specifica per il tipo di protocollo di trasporto usato. Gli errori hanno un nome, un tipo che può essere primitivo o definito dall’utente e una descrizione, la quale è usata per generare commenti e documentazione.

Questo documento descrive come definire errori nei file di design di Goa e come sfruttare il codice generato per ritornare errori dai service moethods.

Design

Il DSL di Goa rende possibile definire errori all’interno dei metodi o globalmente ai servizi attraverso l’espressione Error:

var _ = Service("divider", func() {
    // L'errore "DivByZero" è definito a livello di servizio
    // quindi può essere ritornato sia da "divide" che da "integral_divide".
    Error("DivByZero", func() {
        Description("DivByZero is the error returned by the service methods when the right operand is 0.")
    })

    Method("integral_divide", func() {
        // L'errore "HasRemainder" è definito a livello di service method
        // pertanto è specifico di "integral_divide".
        Error("HasRemainder", func() {
            Description("HasRemainder is the error returned when an integer division has a remainder.")
        })
        // ...
    })

    Method("divide", func() {
        // ...
    })
})

In questo esempio sia DivByZero che HasRemainder usano il tipo di default per gli errori, chiamato ErrorResult. Questo tipo ha i seguenti campi:

  • Name è il nome dell’errore. Il codice generato si prende carico di inizializzare il campo con il nome definito in fase di design durante la deserializzazione della risposta.
  • ID è l’identificatore univoco per un’istanza specifica dell’errore. L’idea è che tale ID sia costruibile, rendendo possibile la correlazione con una richiesta (o un utente) in un servizio di log o error tracing.
  • Message è il messaggio di errore.
  • Temporary indica se l’errore è considerato temporaneo.
  • Timeout indica se l’errore è causato da un timeout.
  • Fault indica se l’errore è causato da un problema nel server.

Il DSL rende possibile di specificare quando un errore denota una condizione temporanea e/o un timeout o un problema server-side.

Error("network_failure", func() {
    Temporary()
})

Error("timeout"), func() {
    Timeout()
})

Error("remote_timeout", func() {
    Temporary()
    Timeout()
})

Error("internal_error", func() {
    Fault()
})

Il codice generato si occupa di inizializzare ErrorResult con i campi Temporary, Timeout e Fault appropriatamente, durante la creazione della error response.

Progettare le error response

La funzione Response rende possibile la definizione di logiche delle risposte HTTP/gRPC associate a un determinato errore.

var _ = Service("divider", func() {
    Error("DivByZero")
    HTTP(func() {
        // Usa lo status HTTP 400 Bad Request per gli errori 
        // "DivByZero".
        Response("DivByZero", StatusBadRequest)
    })
    GRPC(func() {
        // Usa il gRPC status code "InvalidArgument" per l'errore
        // "DivByZero".
        Response("DivByZero", CodeInvalidArgument)
    })

    Method("integral_divide", func() {
        Error("HasRemainder")
        HTTP(func() {
            Response("HasRemainder", StatusExpectationFailed)
            // ...
        })
        GRPC(func() {
          Response("HasRemainder", CodeUnknown)
        })
    })
    // ...
})

Return Errors

Dato il design del servizio divider qui usato come esempio, Goa genera due helper functions che costruiscono i corrispondenti errori: MakeDivByZero e MakeHasRemainder. Tali funzioni accettano un go error come parametro, rendendo conveniente mappare gli errori della logica di business in specifici oggetti ErrorResult.

Ecco un esempio di come un implementazione di integral_divide potrebbe essere:

func (s *dividerSvc) IntegralDivide(ctx context.Context, p *dividersvc.IntOperands) (int, error) {
    if p.B == 0 {
        // Usa la funzione generata per creare un ErrorResult
        return 0, dividersvc.MakeDivByZero(fmt.Errorf("right operand cannot be 0"))
    }
    if p.A%p.B != 0 {
        return 0, dividersvc.MakeHasRemainder(fmt.Errorf("remainder is %d", p.A%p.B))
    }

    return p.A / p.B, nil
}

Ed è fatta! Dato ciò, goa conosce come inizializzare un ErrorResult usando il go error fornito per inizializzare il campo Message e tutti gli altri campi usando le informazioni fornite nel file di design. Il codice per il protocollo di trasporto viene anch’esso generato e comprende i codici di stato HTTP/gRPC come definiti nel Response DSL.

Usa il client da riga di comando generato per verificare:

./dividercli -v divider integer-divide -a 1 -b 2
> GET http://localhost:8080/idiv/1/2
< 417 Expectation Failed
< Content-Length: 68
< Content-Type: application/json
< Date: Thu, 22 Mar 2018 01:34:33 GMT
{"name":"HasRemainder","id":"dlqvenWL","message":"remainder is 1"}

Uso di tipi di errore personalizzati

Il DSL Error permette di specificare un tipo di errore per l’error result specificato, sovrascrivendo il default (corrispondente a ErrorResult). Qualunque tipo può essere usato per dare forma all’error result. Ecco un esempio di come usare una stringa come tipo di errore di ritorno:

Error("NotFound", String, "NotFound is the error returned when there is no bottle with the given ID.")

Nota come la descrizione può essere deifnita inline quando il tipo viene definito esplicitamente. Il tipo può essere ErrorResult, il che rende possibile definire la descrizione inline anche in quel caso.

Tuttavia ci sono un paio di cose a cui fare attenzione quando si usano tipi di errore personalizzati:

  • Le espressioni Temporary, Timeout, e Fault non hanno effetto sul codice generato in questo caso, poiché setterebbero automaticamente i rispettivi campi, ma solo sulla struct ErrorResult.

  • Se il tipo personalizzato è definito dall’utente e se è usato per definire errori multipli sullo stesso metodo, allora goa deve essere informato su quale attributo contiene il nome dell’errore. Il valore di tale attributo viene confrontato con i nomi degli errori definiti in fase di design dal codice di serializzazione/deserializzazione per inferire le corrette proprietà di codifica (es. HTTP status code). L’attributo viene identificato usando lo special tag struct:error:name, il quale deve essere una stringa e deve essere obbligatorio:

var InsertConflict = ResultType("application/vnd.service.insertconflict", func() {
    Description("InsertConflict is the type of the error values returned when insertion fails because of a conflict")
    Attributes(func() {
        Attribute("conflict_value", String)
        Attribute("name", String, "name of error used by goa to encode response", func() {
            Meta("struct:error:name")
        })
        Required("conflict_value", "name")
    })
    View("default", func() {
        Attribute("conflict_value")
        // Nota: error_name viene omesso dalla default view.
        // In questo esempio error_name è un attributo usato per identificare
        // il campo contenente il nome dell'errore e non è
        // serializzato nella risposta al client.
    })
})
  • I tipi definiti dall’utente che definiscono errori personalizzati non possono avere un attributo chiamato error_name, dato che il codice generato crea una funzione chiamata ErrorName sulla error struct.