Improve API error handling
All success responses from the API return a JSON object. When we have errors, a plain string is returned instead.
In this section we are going to improve this, by returning a JSON object when errors occur.
We will need to:
- Create a structure to represent an API error
- Use the structure to return an JSON object when an error occurs
- Validate the return in the API tests
Creating the structure
Go to the API directory.
The error structure needs to be used in all packages.
So it needs to go to a separated package, named apierror
.
Let's create the apierror
directory and add the structure definition.
mkdir apierror
touch apierror/apierror.go
touch apierror/apierror_test.go
The contents of the apierror.go
are:
package apierror
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
)
type ApiError struct {
Message string `json:"message"`
}
func New(message string) ApiError {
return ApiError{Message: message}
}
Let's add a test for the New
constructor:
package apierror
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewApiError(t *testing.T) {
errorMessage := "this is a fake error message"
err := New(errorMessage)
assert.Equal(t, errorMessage, err.Message)
}
Use the API error structure
Let's start by the finance/currconv.go
file.
The errors are handled like the following example:
// ...
if from == "" {
c.JSON(http.StatusBadRequest, "error: 'from' parameter is required")
return
}
// ...
We need to return an ApiError
instead of a plain string:
// ...
if from == "" {
msg := "error: 'from' parameter is required"
c.JSON(http.StatusBadRequest, apierror.New(msg))
return
}
// ...
🏋️♀️ CHALLENGE: do this change for all errors in all packages.
Change the tests
In the error test cases we need to check if the return is valid.
The code to verify if the error code is valid is:
apiError := ApiError{}
err := json.Unmarshal(jsonData, &apiError)
assert.Nil(t, err)
assert.NotEmpty(t, apiError.Message)
We can add this as a function in the apierror
package so we can keep our tests
free of duplicated code. The contents of the apierror.go
file are:
package apierror
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
)
type ApiError struct {
Message string `json:"message"`
}
func New(message string) ApiError {
return ApiError{Message: message}
}
func AssertIsValid(t *testing.T, jsonData []byte) { // ne
apiError := ApiError{}
err := json.Unmarshal(jsonData, &apiError)
assert.Nil(t, err)
assert.NotEmpty(t, apiError.Message)
}
The AssertIsValid
function also needs to be tested. Add the following to the
apierror_test.go
file:
func TestAssertIsValid(t *testing.T) {
// arrange
err := New("this is a fake error message")
data, _ := json.Marshal(err)
tt := testing.T{}
// act
AssertIsValid(&tt, data)
// assert
assert.False(t, tt.Failed())
}
func TestAssertIsValidWithInvalidJson(t *testing.T) {
// arrange
data := []byte("this is an invalid json")
tt := testing.T{}
// act
AssertIsValid(&tt, data)
// assert
assert.True(t, tt.Failed())
}
In the currconv_test.go
we can now use the AssertIsValid
function to check
the return.
For example:
func TestGetCurrConvWithMissingFrom(t *testing.T) {
// arrange
to := "USD"
amount := 10.0
mockInterface := financelib.MockInterface{}
r := setupGin(&mockInterface)
w := httptest.NewRecorder()
url := fmt.Sprintf("/v1/finance/currconv?to=%s&amount=%f", to, amount)
req, _ := http.NewRequest("GET", url, nil)
// act
r.ServeHTTP(w, req)
// assert
assert.Equal(t, w.Code, http.StatusBadRequest)
apierror.AssertIsValid(t, w.Body.Bytes()) // asserting the error
}
🏋️♀️ CHALLENGE: do this change for the tests in all packages.
In the end, ensure all tests are green.
Manual testing
To see the output, let's do an invalid request using httpie
.
Start the API:
go run main.go
And make the request:
http localhost:8080/v1/finance/currconv
The result should be similar to:
HTTP/1.1 400 Bad Request
Content-Length: 49
Content-Type: application/json; charset=utf-8
Date: Sun, 02 Jan 2022 11:47:24 GMT
{
"message": "error: 'from' parameter is required"
}
Wrap up
Commit and push everything. Create a new tag.
git add .
git commit -m "refactor: return json on error cases"
git push
git tag -a v0.0.6 -m "v0.0.6"
git push origin v0.0.6
After some minutes we should have the new version installed in our k8s cluster.
Next
The next section is Add structured logs to the API.