From fcbcda7eac5c888f8a8eb561e5f43e9e8af2cd23 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Thu, 3 Sep 2026 19:09:40 -0400 Subject: [PATCH] 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 --- AniListFunctions.go | 75 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 64 insertions(+), 11 deletions(-) diff --git a/AniListFunctions.go b/AniListFunctions.go index 13a8d49..7b7bcf3 100644 --- a/AniListFunctions.go +++ b/AniListFunctions.go @@ -7,26 +7,42 @@ import ( "io" "log" "net/http" + "strings" + "time" ) func AniListQuery(body interface{}, login bool) (json.RawMessage, string) { - reader, _ := json.Marshal(body) - response, err := http.NewRequest("POST", "https://graphql.anilist.co", bytes.NewBuffer(reader)) + reader, err := json.Marshal(body) 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 { - response.Header.Add("Authorization", "Bearer "+aniListJwt.AccessToken) + request.Header.Add("Authorization", "Bearer "+aniListJwt.AccessToken) } else if login { return nil, "Please login to AniList to make this request" } - response.Header.Add("Content-Type", "application/json") - response.Header.Add("Accept", "application/json") + request.Header.Add("Content-Type", "application/json") + request.Header.Add("Accept", "application/json") - client := &http.Client{} - res, resErr := client.Do(response) + client := &http.Client{Timeout: 20 * time.Second} + res, resErr := client.Do(request) 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() @@ -39,6 +55,37 @@ func AniListQuery(body interface{}, login bool) (json.RawMessage, string) { 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 { user := a.GetAniListLoggedInUser() @@ -338,12 +385,18 @@ func (a *App) AniListSearch(query string) (interface{}, error) { } returnedBody, status := AniListQuery(body, false) 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{} err := json.Unmarshal(returnedBody, &post) 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 post, nil