Skip to content

Commit

Permalink
#1: Fix Mojang's API HTTPClient default configuration, make mojang.Re…
Browse files Browse the repository at this point in the history
…sponseError interface not applicable to any type, add handling of some possible network errors
  • Loading branch information
erickskrauch committed Apr 21, 2019
1 parent a8bbacf commit 7d1506d
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 135 deletions.
61 changes: 51 additions & 10 deletions api/mojang/mojang.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import (
"encoding/json"
"io/ioutil"
"net/http"
"time"
)

var HttpClient = &http.Client{}
var HttpClient = &http.Client{
Timeout: 3 * time.Second,
}

type SignedTexturesResponse struct {
Id string `json:"id"`
Expand All @@ -32,10 +35,7 @@ type ProfileInfo struct {
// See https://wiki.vg/Mojang_API#Playernames_-.3E_UUIDs
func UsernamesToUuids(usernames []string) ([]*ProfileInfo, error) {
requestBody, _ := json.Marshal(usernames)
request, err := http.NewRequest("POST", "https://api.mojang.com/profiles/minecraft", bytes.NewBuffer(requestBody))
if err != nil {
panic(err)
}
request, _ := http.NewRequest("POST", "https://api.mojang.com/profiles/minecraft", bytes.NewBuffer(requestBody))

request.Header.Set("Content-Type", "application/json")

Expand Down Expand Up @@ -65,10 +65,7 @@ func UuidToTextures(uuid string, signed bool) (*SignedTexturesResponse, error) {
url += "?unsigned=false"
}

request, err := http.NewRequest("GET", url, nil)
if err != nil {
panic(err)
}
request, _ := http.NewRequest("GET", url, nil)

response, err := HttpClient.Do(request)
if err != nil {
Expand All @@ -92,15 +89,30 @@ func validateResponse(response *http.Response) error {
switch {
case response.StatusCode == 204:
return &EmptyResponse{}
case response.StatusCode == 400:
type errorResponse struct {
Error string `json:"error"`
Message string `json:"errorMessage"`
}

var decodedError *errorResponse
body, _ := ioutil.ReadAll(response.Body)
_ = json.Unmarshal(body, &decodedError)

return &BadRequestError{ErrorType: decodedError.Error, Message: decodedError.Message}
case response.StatusCode == 429:
return &TooManyRequestsError{}
case response.StatusCode >= 500:
return &ServerError{response.StatusCode}
return &ServerError{Status: response.StatusCode}
}

return nil
}

type ResponseError interface {
IsMojangError() bool
}

// Mojang API doesn't return a 404 Not Found error for non-existent data identifiers
// Instead, they return 204 with an empty body
type EmptyResponse struct {
Expand All @@ -110,19 +122,48 @@ func (*EmptyResponse) Error() string {
return "Empty Response"
}

func (*EmptyResponse) IsMojangError() bool {
return true
}

// When passed request params are invalid, Mojang returns 400 Bad Request error
type BadRequestError struct {
ResponseError
ErrorType string
Message string
}

func (e *BadRequestError) Error() string {
return e.Message
}

func (*BadRequestError) IsMojangError() bool {
return true
}

// When you exceed the set limit of requests, this error will be returned
type TooManyRequestsError struct {
ResponseError
}

func (*TooManyRequestsError) Error() string {
return "Too Many Requests"
}

func (*TooManyRequestsError) IsMojangError() bool {
return true
}

// ServerError happens when Mojang's API returns any response with 50* status
type ServerError struct {
ResponseError
Status int
}

func (e *ServerError) Error() string {
return "Server error"
}

func (*ServerError) IsMojangError() bool {
return true
}
29 changes: 29 additions & 0 deletions api/mojang/mojang_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,30 @@ func TestUsernamesToUuids(t *testing.T) {
}
})

t.Run("handle bad request response", func(t *testing.T) {
assert := testify.New(t)

defer gock.Off()
gock.New("https://api.mojang.com").
Post("/profiles/minecraft").
Reply(400).
JSON(map[string]interface{}{
"error": "IllegalArgumentException",
"errorMessage": "profileName can not be null or empty.",
})

client := &http.Client{}
gock.InterceptClient(client)

HttpClient = client

result, err := UsernamesToUuids([]string{""})
assert.Nil(result)
assert.IsType(&BadRequestError{}, err)
assert.EqualError(err, "profileName can not be null or empty.")
assert.Implements((*ResponseError)(nil), err)
})

t.Run("handle too many requests response", func(t *testing.T) {
assert := testify.New(t)

Expand All @@ -72,6 +96,7 @@ func TestUsernamesToUuids(t *testing.T) {
assert.Nil(result)
assert.IsType(&TooManyRequestsError{}, err)
assert.EqualError(err, "Too Many Requests")
assert.Implements((*ResponseError)(nil), err)
})

t.Run("handle server error", func(t *testing.T) {
Expand All @@ -93,6 +118,7 @@ func TestUsernamesToUuids(t *testing.T) {
assert.IsType(&ServerError{}, err)
assert.EqualError(err, "Server error")
assert.Equal(500, err.(*ServerError).Status)
assert.Implements((*ResponseError)(nil), err)
})
}

Expand Down Expand Up @@ -185,6 +211,7 @@ func TestUuidToTextures(t *testing.T) {
assert.Nil(result)
assert.IsType(&EmptyResponse{}, err)
assert.EqualError(err, "Empty Response")
assert.Implements((*ResponseError)(nil), err)
})

t.Run("handle too many requests response", func(t *testing.T) {
Expand All @@ -208,6 +235,7 @@ func TestUuidToTextures(t *testing.T) {
assert.Nil(result)
assert.IsType(&TooManyRequestsError{}, err)
assert.EqualError(err, "Too Many Requests")
assert.Implements((*ResponseError)(nil), err)
})

t.Run("handle server error", func(t *testing.T) {
Expand All @@ -229,5 +257,6 @@ func TestUuidToTextures(t *testing.T) {
assert.IsType(&ServerError{}, err)
assert.EqualError(err, "Server error")
assert.Equal(500, err.(*ServerError).Status)
assert.Implements((*ResponseError)(nil), err)
})
}
44 changes: 33 additions & 11 deletions api/mojang/queue/queue.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package queue

import (
"net"
"regexp"
"strings"
"sync"
"syscall"
"time"

"github.com/elyby/chrly/api/mojang"
Expand Down Expand Up @@ -100,15 +102,15 @@ func (ctx *JobsQueue) queueRound() {
}

profiles, err := usernamesToUuids(usernames)
switch err.(type) {
case *mojang.TooManyRequestsError, *mojang.ServerError:
for _, job := range jobs {
job.RespondTo <- nil
}
if err != nil {
defer func() {
for _, job := range jobs {
job.RespondTo <- nil
}
}()
maybeShouldPanic(err)

return
case error:
panic(err)
}

var wg sync.WaitGroup
Expand Down Expand Up @@ -146,11 +148,9 @@ func (ctx *JobsQueue) getTextures(uuid string) *mojang.SignedTexturesResponse {

shouldCache := true
result, err := uuidToTextures(uuid, true)
switch err.(type) {
case *mojang.EmptyResponse, *mojang.TooManyRequestsError, *mojang.ServerError:
if err != nil {
maybeShouldPanic(err)
shouldCache = false
case error:
panic(err)
}

if shouldCache && result != nil {
Expand All @@ -159,3 +159,25 @@ func (ctx *JobsQueue) getTextures(uuid string) *mojang.SignedTexturesResponse {

return result
}

// Starts to panic if there's an unexpected error
func maybeShouldPanic(err error) {
switch err.(type) {
case mojang.ResponseError:
return
case net.Error:
if err.(net.Error).Timeout() {
return
}

if opErr, ok := err.(*net.OpError); ok && (opErr.Op == "dial" || opErr.Op == "read") {
return
}

if err == syscall.ECONNREFUSED {
return
}
}

panic(err)
}

0 comments on commit 7d1506d

Please sign in to comment.