Skip to content

Commit 266ee18

Browse files
authored
Merge pull request #63 from messagebird/fix-voice-error-response
Fix voice error response
2 parents a737548 + 9e497ba commit 266ee18

File tree

6 files changed

+233
-21
lines changed

6 files changed

+233
-21
lines changed

README.md

Lines changed: 66 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ This repository contains the open source Go client for MessageBird's REST API. D
77
Requirements
88
------------
99
- [Sign up](https://www.messagebird.com/en/signup) for a free MessageBird account
10-
- Create a new access key in the developers sections
10+
- Create a new access key in the [dashboard](https://dashboard.messagebird.com/en-us/developers/access).
1111
- An application written in Go to make use of this API
1212

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

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

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

35-
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:
35+
// Create a client.
36+
client := messagebird.New(accessKey)
3637

37-
```go
38-
// Request the balance information, returned as a Balance object.
38+
// Request the balance information, returned as a balance.Balance object.
3939
balance, err := balance.Read(client)
4040
if err != nil {
41-
switch errResp := err.(type) {
42-
case messagebird.ErrorResponse:
43-
for _, mbError := range errResp.Errors {
44-
fmt.Printf("Error: %#v\n", mbError)
45-
}
46-
}
47-
41+
// Handle error.
4842
return
4943
}
5044

51-
fmt.Println(" payment :", balance.Payment)
52-
fmt.Println(" type :", balance.Type)
53-
fmt.Println(" amount :", balance.Amount)
45+
// Display the results.
46+
fmt.Println("Payment: ", balance.Payment)
47+
fmt.Println("Type:", balance.Type)
48+
fmt.Println("Amount:", balance.Amount)
5449
```
5550

5651
This will give you something like:
57-
```shell
52+
53+
```bash
5854
$ go run example.go
59-
payment : prepaid
60-
type : credits
61-
amount : 9
55+
Payment: prepaid
56+
Type: credits
57+
Amount: 9
6258
```
6359

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

62+
Errors
63+
------
64+
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.
65+
66+
It is important to notice that the Voice API returns errors with a format that slightly differs from other APIs.
67+
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`.
68+
69+
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:
70+
71+
```go
72+
import "github.com/messagebird/go-rest-api"
73+
import "github.com/messagebird/go-rest-api/sms"
74+
75+
// ...
76+
77+
_, err := sms.Read(client, "some-id")
78+
if err != nil {
79+
mbErr, ok := err.(messagebird.ErrorResponse)
80+
if !ok {
81+
// A non-MessageBird error occurred (no connection, perhaps?)
82+
return err
83+
}
84+
85+
fmt.Println("Code:", mbErr.Errors[0].Code)
86+
fmt.Println("Description:", mbErr.Errors[0].Description)
87+
fmt.Println("Parameter:", mbErr.Errors[0].Parameter)
88+
}
89+
```
90+
91+
`voice.ErrorResponse` is very similar, except that it holds `voice.Error` structs - those contain only `Code` and `Message` (not description!) fields:
92+
93+
```go
94+
import "github.com/messagebird/go-rest-api/voice"
95+
96+
// ...
97+
98+
_, err := voice.CallFlowByID(client, "some-id")
99+
if err != nil {
100+
vErr, ok := err.(voice.ErrorResponse)
101+
if !ok {
102+
// A non-MessageBird (Voice) error occurred (no connection, perhaps?)
103+
return err
104+
}
105+
106+
fmt.Println("Code:", vErr.Errors[0].Code)
107+
fmt.Println("Message:", vErr.Errors[0].Message)
108+
}
109+
```
110+
66111
Documentation
67112
-------------
68113
Complete documentation, instructions, and examples are available at:

client.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ const (
3232

3333
// httpClientTimeout is used to limit http.Client waiting time.
3434
httpClientTimeout = 15 * time.Second
35+
36+
// voiceHost is the host name for the Voice API.
37+
voiceHost = "voice.messagebird.com"
3538
)
3639

3740
var (
@@ -55,6 +58,11 @@ const (
5558
contentTypeFormURLEncoded contentType = "application/x-www-form-urlencoded"
5659
)
5760

61+
// errorReader reads the provided byte slice into an appropriate error.
62+
type errorReader func([]byte) error
63+
64+
var voiceErrorReader errorReader
65+
5866
// New creates a new MessageBird client object.
5967
func New(accessKey string) *Client {
6068
return &Client{
@@ -65,6 +73,12 @@ func New(accessKey string) *Client {
6573
}
6674
}
6775

76+
// SetVoiceErrorReader takes an errorReader that must parse raw JSON errors
77+
// returned from the Voice API.
78+
func SetVoiceErrorReader(r errorReader) {
79+
voiceErrorReader = r
80+
}
81+
6882
// Request is for internal use only and unstable.
6983
func (c *Client) Request(v interface{}, method, path string, data interface{}) error {
7084
if !strings.HasPrefix(path, "https://") && !strings.HasPrefix(path, "http://") {
@@ -135,6 +149,10 @@ func (c *Client) Request(v interface{}, method, path string, data interface{}) e
135149
return ErrUnexpectedResponse
136150
default:
137151
// Anything else than a 200/201/204/500 should be a JSON error.
152+
if uri.Host == voiceHost && voiceErrorReader != nil {
153+
return voiceErrorReader(responseBody)
154+
}
155+
138156
var errorResponse ErrorResponse
139157
if err := json.Unmarshal(responseBody, &errorResponse); err != nil {
140158
return err

voice/testdata/error.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"data": null,
3+
"errors": [
4+
{
5+
"code": 13,
6+
"message": "some-error"
7+
}
8+
]
9+
}

voice/testdata/errors.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"data": null,
3+
"errors": [
4+
{
5+
"code": 11,
6+
"message": "some-error"
7+
},
8+
{
9+
"code": 15,
10+
"message": "other-error"
11+
}
12+
]
13+
}

voice/voice.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,52 @@
11
package voice
22

3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/messagebird/go-rest-api"
9+
)
10+
311
const apiRoot = "https://voice.messagebird.com"
12+
13+
type ErrorResponse struct {
14+
Errors []Error
15+
}
16+
17+
type Error struct {
18+
Code int
19+
Message string
20+
}
21+
22+
func init() {
23+
// The Voice API returns errors in a format that slightly differs from other
24+
// APIs. Here we instruct package messagebird to use our custom
25+
// voice.errorReader func, which has access to voice.ErrorResponse, to
26+
// unmarshal those. Package messagebird must not import the voice package to
27+
// safeguard against import cycles, so it can not use voice.ErrorResponse
28+
// directly.
29+
messagebird.SetVoiceErrorReader(errorReader)
30+
}
31+
32+
// errorReader takes a []byte representation of a Voice API JSON error and
33+
// parses it to a voice.ErrorResponse.
34+
func errorReader(b []byte) error {
35+
var er ErrorResponse
36+
if err := json.Unmarshal(b, &er); err != nil {
37+
return fmt.Errorf("encoding/json: Unmarshal: %v", err)
38+
}
39+
return er
40+
}
41+
42+
func (e ErrorResponse) Error() string {
43+
errStrings := make([]string, len(e.Errors))
44+
for i, v := range e.Errors {
45+
errStrings[i] = v.Error()
46+
}
47+
return strings.Join(errStrings, "; ")
48+
}
49+
50+
func (e Error) Error() string {
51+
return fmt.Sprintf("code: %d, message: %q", e.Code, e.Message)
52+
}

voice/voice_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package voice
2+
3+
import (
4+
"testing"
5+
6+
"github.com/messagebird/go-rest-api/internal/mbtest"
7+
)
8+
9+
func TestErrorReader(t *testing.T) {
10+
t.Run("Single error", func(t *testing.T) {
11+
b := mbtest.Testdata(t, "error.json")
12+
err := errorReader(b).(ErrorResponse)
13+
14+
if count := len(err.Errors); count != 1 {
15+
t.Fatalf("Got %d, expected 1", count)
16+
}
17+
18+
if err.Errors[0].Code != 13 {
19+
t.Errorf("Got %d, expected 13", err.Errors[0].Code)
20+
}
21+
if err.Errors[0].Message != "some-error" {
22+
t.Errorf("Got %q, expected some-error", err.Errors[0].Message)
23+
}
24+
})
25+
26+
t.Run("Multiple errors", func(t *testing.T) {
27+
b := mbtest.Testdata(t, "errors.json")
28+
err := errorReader(b).(ErrorResponse)
29+
30+
if count := len(err.Errors); count != 2 {
31+
t.Fatalf("Got %d, expected 2", count)
32+
}
33+
34+
if err.Errors[0].Code != 11 {
35+
t.Errorf("Got %d, expected 11", err.Errors[0].Code)
36+
}
37+
if err.Errors[0].Message != "some-error" {
38+
t.Errorf("Got %q, expected some-error", err.Errors[0].Message)
39+
}
40+
if err.Errors[1].Code != 15 {
41+
t.Errorf("Got %d, expected 15", err.Errors[1].Code)
42+
}
43+
if err.Errors[1].Message != "other-error" {
44+
t.Errorf("Got %q, expected other-error", err.Errors[1].Message)
45+
}
46+
})
47+
48+
t.Run("Invalid JSON", func(t *testing.T) {
49+
b := []byte("clearly not json")
50+
_, ok := errorReader(b).(ErrorResponse)
51+
52+
if ok {
53+
// If the data b is not JSON, we expect a "generic" errorString
54+
// (from fmt.Errorf), but we somehow got our own ErrorResponse back.
55+
t.Fatalf("Got ErrorResponse, expected errorString")
56+
}
57+
})
58+
}
59+
60+
func TestErrorResponseError(t *testing.T) {
61+
err := ErrorResponse{
62+
[]Error{
63+
{
64+
Code: 1,
65+
Message: "foo",
66+
},
67+
{
68+
Code: 2,
69+
Message: "bar",
70+
},
71+
},
72+
}
73+
74+
expect := `code: 1, message: "foo"; code: 2, message: "bar"`
75+
if actual := err.Error(); actual != expect {
76+
t.Fatalf("Got %q, expected %q", actual, expect)
77+
}
78+
}

0 commit comments

Comments
 (0)