fix(anilist): harden AniList query transport and search errors
AniListQuery previously ignored json.Marshal/http.NewRequest errors, logged the wrong variable on client.Do failure, and dereferenced a nil response body, which could panic and leave callers hanging. - Return early with user-safe messages on encode/request failures - Add 20s http.Client timeout and nil guards for res/res.Body - Keep logs generic so no auth material is ever printed - Add aniListGraphQLErrorMessage/aniListSearchStatusError helpers mapping 429 to a rate-limit retry message, 5xx to a temporary outage message, and GraphQL errors[] (even on HTTP 200) to a surfaced message - AniListSearch rejects empty bodies and unparseable payloads with actionable errors instead of failing silently downstream
This commit is contained in:
+64
-11
@@ -7,26 +7,42 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func AniListQuery(body interface{}, login bool) (json.RawMessage, string) {
|
func AniListQuery(body interface{}, login bool) (json.RawMessage, string) {
|
||||||
reader, _ := json.Marshal(body)
|
reader, err := json.Marshal(body)
|
||||||
response, err := http.NewRequest("POST", "https://graphql.anilist.co", bytes.NewBuffer(reader))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed at response, %s\n", err)
|
log.Printf("AniList request failed: could not encode request body\n")
|
||||||
|
return nil, "Could not prepare the AniList request."
|
||||||
|
}
|
||||||
|
request, err := http.NewRequest("POST", "https://graphql.anilist.co", bytes.NewBuffer(reader))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("AniList request failed: could not create request\n")
|
||||||
|
return nil, "Could not reach AniList. Please check your connection and try again."
|
||||||
}
|
}
|
||||||
if login && (AniListJWT{}) != aniListJwt {
|
if login && (AniListJWT{}) != aniListJwt {
|
||||||
response.Header.Add("Authorization", "Bearer "+aniListJwt.AccessToken)
|
request.Header.Add("Authorization", "Bearer "+aniListJwt.AccessToken)
|
||||||
} else if login {
|
} else if login {
|
||||||
return nil, "Please login to AniList to make this request"
|
return nil, "Please login to AniList to make this request"
|
||||||
}
|
}
|
||||||
response.Header.Add("Content-Type", "application/json")
|
request.Header.Add("Content-Type", "application/json")
|
||||||
response.Header.Add("Accept", "application/json")
|
request.Header.Add("Accept", "application/json")
|
||||||
|
|
||||||
client := &http.Client{}
|
client := &http.Client{Timeout: 20 * time.Second}
|
||||||
res, resErr := client.Do(response)
|
res, resErr := client.Do(request)
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
log.Printf("Failed at res, %s\n", err)
|
log.Printf("AniList request failed: network error\n")
|
||||||
|
return nil, "Could not reach AniList. Please check your connection and try again."
|
||||||
|
}
|
||||||
|
if res == nil {
|
||||||
|
log.Printf("AniList request failed: empty response\n")
|
||||||
|
return nil, "Could not reach AniList. Please check your connection and try again."
|
||||||
|
}
|
||||||
|
if res.Body == nil {
|
||||||
|
log.Printf("AniList request failed: empty response body\n")
|
||||||
|
return nil, "Could not reach AniList. Please check your connection and try again."
|
||||||
}
|
}
|
||||||
|
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
@@ -39,6 +55,37 @@ func AniListQuery(body interface{}, login bool) (json.RawMessage, string) {
|
|||||||
return returnedBody, res.Status
|
return returnedBody, res.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func aniListGraphQLErrorMessage(returnedBody json.RawMessage) string {
|
||||||
|
var gqlErr struct {
|
||||||
|
Errors []struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
} `json:"errors"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(returnedBody, &gqlErr); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if len(gqlErr.Errors) > 0 && strings.TrimSpace(gqlErr.Errors[0].Message) != "" {
|
||||||
|
return strings.TrimSpace(gqlErr.Errors[0].Message)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func aniListSearchStatusError(status string, returnedBody json.RawMessage) error {
|
||||||
|
if msg := aniListGraphQLErrorMessage(returnedBody); msg != "" {
|
||||||
|
return fmt.Errorf("AniList search failed: %s", msg)
|
||||||
|
}
|
||||||
|
if strings.Contains(status, "429") {
|
||||||
|
return fmt.Errorf("AniList is rate-limiting search right now. Please wait a moment and try again.")
|
||||||
|
}
|
||||||
|
if strings.Contains(status, "500") || strings.Contains(status, "502") || strings.Contains(status, "503") || strings.Contains(status, "504") {
|
||||||
|
return fmt.Errorf("AniList is temporarily unavailable (%s). Please try again shortly.", status)
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(status, "Could not") || strings.HasPrefix(status, "Please login") {
|
||||||
|
return fmt.Errorf("%s", status)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("AniList search failed (%s). Please try again.", status)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) GetAniListItem(aniId int, login bool) AniListGetSingleAnime {
|
func (a *App) GetAniListItem(aniId int, login bool) AniListGetSingleAnime {
|
||||||
user := a.GetAniListLoggedInUser()
|
user := a.GetAniListLoggedInUser()
|
||||||
|
|
||||||
@@ -338,12 +385,18 @@ func (a *App) AniListSearch(query string) (interface{}, error) {
|
|||||||
}
|
}
|
||||||
returnedBody, status := AniListQuery(body, false)
|
returnedBody, status := AniListQuery(body, false)
|
||||||
if status != "200 OK" {
|
if status != "200 OK" {
|
||||||
return nil, fmt.Errorf("API search failed with status: %s", status)
|
return nil, aniListSearchStatusError(status, returnedBody)
|
||||||
|
}
|
||||||
|
if len(returnedBody) == 0 {
|
||||||
|
return nil, fmt.Errorf("AniList returned an empty response. Please try again.")
|
||||||
|
}
|
||||||
|
if msg := aniListGraphQLErrorMessage(returnedBody); msg != "" {
|
||||||
|
return nil, fmt.Errorf("AniList search failed: %s", msg)
|
||||||
}
|
}
|
||||||
var post interface{}
|
var post interface{}
|
||||||
err := json.Unmarshal(returnedBody, &post)
|
err := json.Unmarshal(returnedBody, &post)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed at unmarshal, %s\n", err)
|
log.Printf("Failed at unmarshal search results\n")
|
||||||
return nil, fmt.Errorf("Failed to parse search results")
|
return nil, fmt.Errorf("Failed to parse search results")
|
||||||
}
|
}
|
||||||
return post, nil
|
return post, nil
|
||||||
|
|||||||
Reference in New Issue
Block a user