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:
2026-09-03 19:09:40 -04:00
parent 0a89ec3652
commit fcbcda7eac
+64 -11
View File
@@ -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