Skip to content

Commit

Permalink
Merge pull request #63 from messagebird/fix-voice-error-response
Browse files Browse the repository at this point in the history
Fix voice error response
  • Loading branch information
marcelcorso authored Aug 12, 2019
2 parents a737548 + 9e497ba commit 266ee18
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 21 deletions.
87 changes: 66 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This repository contains the open source Go client for MessageBird's REST API. D
Requirements
------------
- [Sign up](https://www.messagebird.com/en/signup) for a free MessageBird account
- Create a new access key in the developers sections
- Create a new access key in the [dashboard](https://dashboard.messagebird.com/en-us/developers/access).
- An application written in Go to make use of this API

Installation
Expand All @@ -26,43 +26,88 @@ Here is a quick example on how to get started. Assuming the **go get** installat
import "github.com/messagebird/go-rest-api"
```

Then, create an instance of **messagebird.Client**:
Then, create an instance of **messagebird.Client**. It can be used to access the MessageBird APIs.

```go
client := messagebird.New("test_gshuPaZoeEG6ovbc8M79w0QyM")
```
// Access keys can be managed through our dashboard.
accessKey := "your-access-key"

Now you can query the API for information or send data. For example, if we want to request our balance information you'd do something like this:
// Create a client.
client := messagebird.New(accessKey)

```go
// Request the balance information, returned as a Balance object.
// Request the balance information, returned as a balance.Balance object.
balance, err := balance.Read(client)
if err != nil {
switch errResp := err.(type) {
case messagebird.ErrorResponse:
for _, mbError := range errResp.Errors {
fmt.Printf("Error: %#v\n", mbError)
}
}

// Handle error.
return
}

fmt.Println(" payment :", balance.Payment)
fmt.Println(" type :", balance.Type)
fmt.Println(" amount :", balance.Amount)
// Display the results.
fmt.Println("Payment: ", balance.Payment)
fmt.Println("Type:", balance.Type)
fmt.Println("Amount:", balance.Amount)
```

This will give you something like:
```shell

```bash
$ go run example.go
payment : prepaid
type : credits
amount : 9
Payment: prepaid
Type: credits
Amount: 9
```

Please see the other examples for a complete overview of all the available API calls.

Errors
------
When something goes wrong, our APIs can return more than a single error. They are therefore returned by the client as "error responses" that contain a slice of errors.

It is important to notice that the Voice API returns errors with a format that slightly differs from other APIs.
For this reason, errors returned by the `voice` package are of type `voice.ErrorResponse`. It contains `voice.Error` structs. All other packages return `messagebird.ErrorResponse` structs that contain a slice of `messagebird.Error`.

An example of "simple" error handling is shown in the example above. Let's look how we can gain more in-depth insight in what exactly went wrong:

```go
import "github.com/messagebird/go-rest-api"
import "github.com/messagebird/go-rest-api/sms"

// ...

_, err := sms.Read(client, "some-id")
if err != nil {
mbErr, ok := err.(messagebird.ErrorResponse)
if !ok {
// A non-MessageBird error occurred (no connection, perhaps?)
return err
}

fmt.Println("Code:", mbErr.Errors[0].Code)
fmt.Println("Description:", mbErr.Errors[0].Description)
fmt.Println("Parameter:", mbErr.Errors[0].Parameter)
}
```

`voice.ErrorResponse` is very similar, except that it holds `voice.Error` structs - those contain only `Code` and `Message` (not description!) fields:

```go
import "github.com/messagebird/go-rest-api/voice"

// ...

_, err := voice.CallFlowByID(client, "some-id")
if err != nil {
vErr, ok := err.(voice.ErrorResponse)
if !ok {
// A non-MessageBird (Voice) error occurred (no connection, perhaps?)
return err
}

fmt.Println("Code:", vErr.Errors[0].Code)
fmt.Println("Message:", vErr.Errors[0].Message)
}
```

Documentation
-------------
Complete documentation, instructions, and examples are available at:
Expand Down
18 changes: 18 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ const (

// httpClientTimeout is used to limit http.Client waiting time.
httpClientTimeout = 15 * time.Second

// voiceHost is the host name for the Voice API.
voiceHost = "voice.messagebird.com"
)

var (
Expand All @@ -55,6 +58,11 @@ const (
contentTypeFormURLEncoded contentType = "application/x-www-form-urlencoded"
)

// errorReader reads the provided byte slice into an appropriate error.
type errorReader func([]byte) error

var voiceErrorReader errorReader

// New creates a new MessageBird client object.
func New(accessKey string) *Client {
return &Client{
Expand All @@ -65,6 +73,12 @@ func New(accessKey string) *Client {
}
}

// SetVoiceErrorReader takes an errorReader that must parse raw JSON errors
// returned from the Voice API.
func SetVoiceErrorReader(r errorReader) {
voiceErrorReader = r
}

// Request is for internal use only and unstable.
func (c *Client) Request(v interface{}, method, path string, data interface{}) error {
if !strings.HasPrefix(path, "https://") && !strings.HasPrefix(path, "http://") {
Expand Down Expand Up @@ -135,6 +149,10 @@ func (c *Client) Request(v interface{}, method, path string, data interface{}) e
return ErrUnexpectedResponse
default:
// Anything else than a 200/201/204/500 should be a JSON error.
if uri.Host == voiceHost && voiceErrorReader != nil {
return voiceErrorReader(responseBody)
}

var errorResponse ErrorResponse
if err := json.Unmarshal(responseBody, &errorResponse); err != nil {
return err
Expand Down
9 changes: 9 additions & 0 deletions voice/testdata/error.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"data": null,
"errors": [
{
"code": 13,
"message": "some-error"
}
]
}
13 changes: 13 additions & 0 deletions voice/testdata/errors.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"data": null,
"errors": [
{
"code": 11,
"message": "some-error"
},
{
"code": 15,
"message": "other-error"
}
]
}
49 changes: 49 additions & 0 deletions voice/voice.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,52 @@
package voice

import (
"encoding/json"
"fmt"
"strings"

"github.com/messagebird/go-rest-api"
)

const apiRoot = "https://voice.messagebird.com"

type ErrorResponse struct {
Errors []Error
}

type Error struct {
Code int
Message string
}

func init() {
// The Voice API returns errors in a format that slightly differs from other
// APIs. Here we instruct package messagebird to use our custom
// voice.errorReader func, which has access to voice.ErrorResponse, to
// unmarshal those. Package messagebird must not import the voice package to
// safeguard against import cycles, so it can not use voice.ErrorResponse
// directly.
messagebird.SetVoiceErrorReader(errorReader)
}

// errorReader takes a []byte representation of a Voice API JSON error and
// parses it to a voice.ErrorResponse.
func errorReader(b []byte) error {
var er ErrorResponse
if err := json.Unmarshal(b, &er); err != nil {
return fmt.Errorf("encoding/json: Unmarshal: %v", err)
}
return er
}

func (e ErrorResponse) Error() string {
errStrings := make([]string, len(e.Errors))
for i, v := range e.Errors {
errStrings[i] = v.Error()
}
return strings.Join(errStrings, "; ")
}

func (e Error) Error() string {
return fmt.Sprintf("code: %d, message: %q", e.Code, e.Message)
}
78 changes: 78 additions & 0 deletions voice/voice_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package voice

import (
"testing"

"github.com/messagebird/go-rest-api/internal/mbtest"
)

func TestErrorReader(t *testing.T) {
t.Run("Single error", func(t *testing.T) {
b := mbtest.Testdata(t, "error.json")
err := errorReader(b).(ErrorResponse)

if count := len(err.Errors); count != 1 {
t.Fatalf("Got %d, expected 1", count)
}

if err.Errors[0].Code != 13 {
t.Errorf("Got %d, expected 13", err.Errors[0].Code)
}
if err.Errors[0].Message != "some-error" {
t.Errorf("Got %q, expected some-error", err.Errors[0].Message)
}
})

t.Run("Multiple errors", func(t *testing.T) {
b := mbtest.Testdata(t, "errors.json")
err := errorReader(b).(ErrorResponse)

if count := len(err.Errors); count != 2 {
t.Fatalf("Got %d, expected 2", count)
}

if err.Errors[0].Code != 11 {
t.Errorf("Got %d, expected 11", err.Errors[0].Code)
}
if err.Errors[0].Message != "some-error" {
t.Errorf("Got %q, expected some-error", err.Errors[0].Message)
}
if err.Errors[1].Code != 15 {
t.Errorf("Got %d, expected 15", err.Errors[1].Code)
}
if err.Errors[1].Message != "other-error" {
t.Errorf("Got %q, expected other-error", err.Errors[1].Message)
}
})

t.Run("Invalid JSON", func(t *testing.T) {
b := []byte("clearly not json")
_, ok := errorReader(b).(ErrorResponse)

if ok {
// If the data b is not JSON, we expect a "generic" errorString
// (from fmt.Errorf), but we somehow got our own ErrorResponse back.
t.Fatalf("Got ErrorResponse, expected errorString")
}
})
}

func TestErrorResponseError(t *testing.T) {
err := ErrorResponse{
[]Error{
{
Code: 1,
Message: "foo",
},
{
Code: 2,
Message: "bar",
},
},
}

expect := `code: 1, message: "foo"; code: 2, message: "bar"`
if actual := err.Error(); actual != expect {
t.Fatalf("Got %q, expected %q", actual, expect)
}
}

0 comments on commit 266ee18

Please sign in to comment.