Skip to content

Commit

Permalink
Enhance reliability of how we make requests and parse errors (#108)
Browse files Browse the repository at this point in the history
  • Loading branch information
sergiught authored Aug 30, 2022
1 parent 7fd1c74 commit d65b4bb
Show file tree
Hide file tree
Showing 9 changed files with 127 additions and 79 deletions.
11 changes: 6 additions & 5 deletions management/anomaly.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,28 @@ func newAnomalyManager(m *Management) *AnomalyManager {
//
// See: https://auth0.com/docs/api/management/v2#!/Anomaly/get_ips_by_id
func (m *AnomalyManager) CheckIP(ip string, opts ...RequestOption) (isBlocked bool, err error) {
req, err := m.NewRequest("GET", m.URI("anomaly", "blocks", "ips", ip), nil, opts...)
request, err := m.NewRequest("GET", m.URI("anomaly", "blocks", "ips", ip), nil, opts...)
if err != nil {
return false, err
}

res, err := m.Do(req)
response, err := m.Do(request)
if err != nil {
return false, err
}
defer response.Body.Close()

// 200: IP address specified is currently blocked.
if res.StatusCode == http.StatusOK {
if response.StatusCode == http.StatusOK {
return true, nil
}

// 404: IP address specified is not currently blocked.
if res.StatusCode == http.StatusNotFound {
if response.StatusCode == http.StatusNotFound {
return false, nil
}

return false, newError(res.Body)
return false, newError(response)
}

// UnblockIP unblocks an IP address currently blocked by the multiple
Expand Down
17 changes: 1 addition & 16 deletions management/branding.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package management
import (
"encoding/json"
"fmt"
"net/http"
)

// Branding is used to customize the look and feel of Auth0 to align
Expand Down Expand Up @@ -168,21 +167,7 @@ func (m *BrandingManager) UniversalLogin(opts ...RequestOption) (ul *BrandingUni
//
// See: https://auth0.com/docs/api/management/v2#!/Branding/put_universal_login
func (m *BrandingManager) SetUniversalLogin(ul *BrandingUniversalLogin, opts ...RequestOption) (err error) {
req, err := m.NewRequest("PUT", m.URI("branding", "templates", "universal-login"), ul.Body, opts...)
if err != nil {
return err
}

res, err := m.Do(req)
if err != nil {
return err
}

if res.StatusCode >= http.StatusBadRequest {
return newError(res.Body)
}

return nil
return m.Request("PUT", m.URI("branding", "templates", "universal-login"), ul.Body, opts...)
}

// DeleteUniversalLogin deletes the template for the New Universal Login Experience.
Expand Down
12 changes: 6 additions & 6 deletions management/guardian.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,23 +146,23 @@ type EnrollmentTicket struct {
//
// See: https://auth0.com/docs/api/management/v2#!/Guardian/post_ticket
func (m *EnrollmentManager) CreateTicket(t *CreateEnrollmentTicket, opts ...RequestOption) (EnrollmentTicket, error) {
req, err := m.NewRequest("POST", m.URI("guardian", "enrollments", "ticket"), t, opts...)
request, err := m.NewRequest("POST", m.URI("guardian", "enrollments", "ticket"), t, opts...)
if err != nil {
return EnrollmentTicket{}, err
}

res, err := m.Do(req)
response, err := m.Do(request)
if err != nil {
return EnrollmentTicket{}, err
}
defer res.Body.Close()
defer response.Body.Close()

if res.StatusCode != http.StatusOK {
return EnrollmentTicket{}, newError(res.Body)
if response.StatusCode != http.StatusOK {
return EnrollmentTicket{}, newError(response)
}

var out EnrollmentTicket
err = json.NewDecoder(res.Body).Decode(&out)
err = json.NewDecoder(response.Body).Decode(&out)
return out, err
}

Expand Down
27 changes: 2 additions & 25 deletions management/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bytes"
"encoding/json"
"mime/multipart"
"net/http"
"net/textproto"
"strconv"
"time"
Expand Down Expand Up @@ -126,29 +125,7 @@ func (m *JobManager) ImportUsers(j *Job, opts ...RequestOption) error {
}
mp.Close()

req, err := http.NewRequest("POST", m.URI("jobs", "users-imports"), &payload)
if err != nil {
return err
}
req.Header.Add("Content-Type", mp.FormDataContentType())

for _, option := range opts {
option.apply(req)
}

res, err := m.Do(req)
if err != nil {
return err
}

if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
return newError(res.Body)
}

if res.StatusCode != http.StatusNoContent {
defer res.Body.Close()
return json.NewDecoder(res.Body).Decode(j)
}
opts = append(opts, Header("Content-Type", mp.FormDataContentType()))

return nil
return m.Request("POST", m.URI("jobs", "users-imports"), &payload, opts...)
}
24 changes: 18 additions & 6 deletions management/management_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package management
import (
"encoding/json"
"fmt"
"io"
"net/http"
)

// Error is an interface describing any error which
Expand All @@ -21,13 +21,25 @@ type managementError struct {
Message string `json:"message"`
}

func newError(r io.Reader) error {
m := &managementError{}
if err := json.NewDecoder(r).Decode(m); err != nil {
return err
func newError(response *http.Response) error {
apiError := &managementError{}

if err := json.NewDecoder(response.Body).Decode(apiError); err != nil {
return &managementError{
StatusCode: response.StatusCode,
Err: http.StatusText(response.StatusCode),
Message: fmt.Errorf("failed to decode json error response payload: %w", err).Error(),
}
}

// This can happen in case the error message structure changes.
// If that happens we still want to display the correct code.
if apiError.Status() == 0 {
apiError.StatusCode = response.StatusCode
apiError.Err = http.StatusText(response.StatusCode)
}

return m
return apiError
}

// Error formats the error into a string representation.
Expand Down
62 changes: 62 additions & 0 deletions management/management_error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package management

import (
"io"
"net/http"
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

func TestNewError(t *testing.T) {
var testCases = []struct {
name string
givenResponse http.Response
expectedError managementError
}{
{
name: "it fails to decode if body is not a json",
givenResponse: http.Response{
StatusCode: http.StatusForbidden,
Body: io.NopCloser(strings.NewReader("Hello, I'm not a JSON.")),
},
expectedError: managementError{
StatusCode: 403,
Err: "Forbidden",
Message: "failed to decode json error response payload: invalid character 'H' looking for beginning of value",
},
},
{
name: "it correctly decodes the error response payload",
givenResponse: http.Response{
StatusCode: http.StatusBadRequest,
Body: io.NopCloser(strings.NewReader(`{"statusCode":400,"error":"Bad Request","message":"One of 'client_id' or 'name' is required."}`)),
},
expectedError: managementError{
StatusCode: 400,
Err: "Bad Request",
Message: "One of 'client_id' or 'name' is required.",
},
},
{
name: "it will still post the correct status code if the body doesn't have the correct structure",
givenResponse: http.Response{
StatusCode: http.StatusInternalServerError,
Body: io.NopCloser(strings.NewReader(`{"errorMessage":"wrongStruct"}`)),
},
expectedError: managementError{
StatusCode: 500,
Err: "Internal Server Error",
Message: "",
},
},
}

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
actualError := newError(&testCase.givenResponse)
assert.Equal(t, &testCase.expectedError, actualError)
})
}
}
27 changes: 16 additions & 11 deletions management/management_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,27 +84,32 @@ func (m *Management) Do(req *http.Request) (*http.Response, error) {
}

// Request combines NewRequest and Do, while also handling decoding of response payload.
func (m *Management) Request(method, uri string, v interface{}, options ...RequestOption) error {
request, err := m.NewRequest(method, uri, v, options...)
func (m *Management) Request(method, uri string, payload interface{}, options ...RequestOption) error {
request, err := m.NewRequest(method, uri, payload, options...)
if err != nil {
return err
return fmt.Errorf("failed to create a new request: %w", err)
}

response, err := m.Do(request)
if err != nil {
return fmt.Errorf("request failed: %w", err)
return fmt.Errorf("failed to send the request: %w", err)
}
defer response.Body.Close()

if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusBadRequest {
return newError(response.Body)
// If the response contains a client or a server error then return the error.
if response.StatusCode >= http.StatusBadRequest {
return newError(response)
}

if response.StatusCode != http.StatusNoContent && response.StatusCode != http.StatusAccepted {
if err := json.NewDecoder(response.Body).Decode(v); err != nil {
return fmt.Errorf("decoding response payload failed: %w", err)
}
responseBody, err := io.ReadAll(response.Body)
if err != nil {
return fmt.Errorf("failed to read the response body: %w", err)
}

return response.Body.Close()
if len(responseBody) > 0 && string(responseBody) != "{}" {
if err = json.Unmarshal(responseBody, &payload); err != nil {
return fmt.Errorf("failed to unmarshal response payload: %w", err)
}
}

return nil
Expand Down
2 changes: 1 addition & 1 deletion management/management_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ func TestOptionParameter(t *testing.T) {
assert.Equal(t, "xyz", bar)
}

func TestOptionDefauls(t *testing.T) {
func TestOptionDefaults(t *testing.T) {
r, _ := http.NewRequest("GET", "/", nil)

applyListDefaults([]RequestOption{
Expand Down
24 changes: 15 additions & 9 deletions management/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package management

import (
"encoding/json"
"fmt"
"io"
"net/http"
"reflect"
"strconv"
Expand Down Expand Up @@ -530,26 +532,30 @@ func (m *UserManager) InvalidateRememberBrowser(id string, opts ...RequestOption
//
// See: https://auth0.com/docs/api/management/v2#!/Users/post_identities
func (m *UserManager) Link(id string, il *UserIdentityLink, opts ...RequestOption) (uIDs []UserIdentity, err error) {
req, err := m.NewRequest("POST", m.URI("users", id, "identities"), il, opts...)
request, err := m.NewRequest("POST", m.URI("users", id, "identities"), il, opts...)
if err != nil {
return uIDs, err
}

res, err := m.Do(req)
response, err := m.Do(request)
if err != nil {
return uIDs, err
}
defer response.Body.Close()

if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
return uIDs, newError(res.Body)
if response.StatusCode >= http.StatusBadRequest {
return uIDs, newError(response)
}

if res.StatusCode != http.StatusNoContent && res.StatusCode != http.StatusAccepted {
err := json.NewDecoder(res.Body).Decode(&uIDs)
if err != nil {
return uIDs, err
responseBody, err := io.ReadAll(response.Body)
if err != nil {
return uIDs, fmt.Errorf("failed to read the response body: %w", err)
}

if len(responseBody) > 0 {
if err = json.Unmarshal(responseBody, &uIDs); err != nil {
return uIDs, fmt.Errorf("failed to unmarshal response payload: %w", err)
}
return uIDs, res.Body.Close()
}

return uIDs, nil
Expand Down

0 comments on commit d65b4bb

Please sign in to comment.