Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Go Error handling on a REST API

Tags:

go

I am very new to go and have deployed a small service with an API endpoint.

I have heard/read that go doesn't use try/catch so I am trying to figure out how I can "catch" any problems happening from my service call from my API and make sure that the resource server doesn't go down.

My code for my API looks like the following..

I have a routes.go file with the following

package main

import (
    "net/http"

    "github.com/gorilla/mux"
)

type Route struct {
    Name        string
    Method      string
    Pattern     string
    HandlerFunc http.HandlerFunc
}

type Routes []Route

func NewRouter() *mux.Router {
    router := mux.NewRouter().StrictSlash(true)
    for _, route := range routes {
        router.
            Methods(route.Method).
            Path(route.Pattern).
            Name(route.Name).
            Handler(route.HandlerFunc)
    }

    return router
}

var routes = Routes{
    Route{
        "CustomerLocationCreate",
        "POST",
        "/tracking/customer",
        CustomerLocationCreate,
    },
}

I have a handlers.go

package main

import (
    "encoding/json"
    "net/http"
    "io"
    "io/ioutil"
)

//curl -H "Content-Type: application/json" -d '{"userId":"1234"}' http://localhost:8181/tracking/customer
func CustomerLocationCreate(w http.ResponseWriter, r *http.Request) {
    var location CustomerLocation
    body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576))
    if err != nil {
        panic(err)
    }
    if err := r.Body.Close(); err != nil {
        panic(err)
    }
    if err := json.Unmarshal(body, &location); err != nil {
        w.Header().Set("Content-Type", "application/json; charset=UTF-8")
        w.WriteHeader(422) // unprocessable entity
        if err := json.NewEncoder(w).Encode(err); err != nil {
            panic(err)
        }
    }

    c := RepoCreateCustomerLocation(location)
    w.Header().Set("Content-Type", "application/json; charset=UTF-8")
    w.WriteHeader(http.StatusCreated)
    if err := json.NewEncoder(w).Encode(c); err != nil {
        panic(err)
    }

    HandleCustomerLocationChange(c);
}

and I have a bus.go which has the HandleCustomerLocationChange(...) function.

func HandleCustomerLocationChange(custLoc CustomerLocation) {

    endpoint := og.Getenv("RABBIT_ENDPOINT")
    conn, err := amqp.Dial("amqp://guest:guest@" + endpoint)
    failOnError(err, "Failed to connect to RabbitMQ")
    defer conn.Close()

    ch, err := conn.Channel()
    failOnError(err, "Failed to open a channel")
    defer ch.Close()

    topic := "locationChange"
    err = ch.ExchangeDeclare(
        topic,   // name
        "topic", // type
        true,    // durable
        false,   // auto-deleted
        false,   // internal
        false,   // no-wait
        nil,     // arguments
    )
    failOnError(err, "Failed to declare an exchange")

    // Create JSON from the instance data.
    body, _ := json.Marshal(custLoc)
    // Convert bytes to string.

    err = ch.Publish(
        topic, // exchange
        "",    // routing key
        false, // mandatory
        false, // immediate
        amqp.Publishing{
            ContentType: "text/plain",
            Body:        body,
        })
    failOnError(err, "Failed to publish a message")

    log.Printf(" [x] Sent %s", body)

}

My question is how should I modify both the HandleCustomerLocationChange(...) function and if necessaryCustomerLocationChange(..)` handler to handle errors properly so that if an error occurs, my entire API doesn't go down?

like image 360
TheJediCowboy Avatar asked Mar 13 '16 22:03

TheJediCowboy


1 Answers

Go suggests a different approach, that errors are not exceptional, they're normal events, just less common.

Taking an example from the code above:

body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576))
if err != nil {
    panic(err)
}

Here, a panic (without recovery) terminates the process, shutting down the web server. Seems an overly severe response to not fully reading a request.

What do you want to do? It may be appropriate to tell the client who made the request:

body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576))
if err != nil {
    http.Error(w, err.Error(), http.StatusBadRequest)
    return
}

You might want to return a json encoded response, or give a generic message to the client avoid exposing too much, and log the specific error details.

For general functions it's idiomatic to return the error as the last return parameter. In the specific example you mentioned:

func HandleCustomerLocationChange(custLoc CustomerLocation) 
...
    conn, err := amqp.Dial(...)
    failOnError(err, "Failed to connect to RabbitMQ")

Instead, check if the connection failed, and return the error to the caller. Handle it in the calling function, or add information and propagate it up the call stack.

func HandleCustomerLocationChange(custLoc CustomerLocation) error
...
    conn, err := amqp.Dial(...)
    if err != nil {
        return fmt.Errorf("failed to connect to RabbitMQ: %s", err)
    }

Propagating the error in this way gives a concise explanation of the root cause, like the 5 whys technique, eg:

"did not update client location: did not connect to rabbitmq: network address 1.2.3 unreachable"

Another convention is to deal with errors first and return early. This helps to reduce nesting.

See also the many error handling resources, like error handling in a web application, Go by Example, Error Handling and Go, errors are values and Defer, Panic & Recover. The source code of the error package is interesting, as is Russ Cox's comment on error handling, and Nathan Youngman's To Err is Human.

Also interesting is Upspin's concept of an operational trace, rather than a stack trace.

like image 163
Mark Avatar answered Oct 16 '22 08:10

Mark