@@ -560,6 +560,102 @@ private Response makeGitHubApiRequest(String url) throws IOException {
560560 return httpClient .newCall (requestBuilder .build ()).execute ();
561561 }
562562
563+ /**
564+ * Builds a user-friendly error message for a failed GitHub API response.
565+ * Consumes the response body if present. Call only when the response is not successful.
566+ *
567+ * @param response the failed HTTP response (body will be consumed)
568+ * @param context description of what was being requested (e.g. "release owner/repo tag v1.0")
569+ * @return a detailed error message including remediation hints
570+ */
571+ private String buildGitHubApiErrorMessage (Response response , String context ) {
572+ int code = response .code ();
573+ String statusMessage = response .message ();
574+ String bodyMessage = null ;
575+ if (response .body () != null ) {
576+ try {
577+ String body = response .body ().string ();
578+ if (body != null && !body .isEmpty ()) {
579+ try {
580+ JsonObject json = gson .fromJson (body , JsonObject .class );
581+ if (json .has ("message" )) {
582+ bodyMessage = json .get ("message" ).getAsString ();
583+ }
584+ } catch (Exception e ) {
585+ bodyMessage = body .length () > 200 ? body .substring (0 , 200 ) + "..." : body ;
586+ }
587+ }
588+ } catch (IOException e ) {
589+ // ignore when reading error body
590+ }
591+ }
592+
593+ StringBuilder msg = new StringBuilder ();
594+ msg .append ("GitHub API request failed for " ).append (context ).append (". " );
595+ msg .append ("HTTP " ).append (code ).append (" " ).append (statusMessage );
596+ if (bodyMessage != null ) {
597+ msg .append (" — " ).append (bodyMessage );
598+ }
599+ msg .append (".\n \n " );
600+
601+ switch (code ) {
602+ case 401 :
603+ msg .append ("FIX: Add a GitHub Personal Access Token so the plugin can access the API.\n " );
604+ if (getApiKey () == null ) {
605+ msg .append (" • In build.gradle add: github { accessToken = \" ghp_YOUR_TOKEN\" }\n " );
606+ msg .append (" • Or set the GITHUB_TOKEN environment variable.\n " );
607+ } else {
608+ msg .append (" • Your token is set but was rejected. Check it is valid and not expired.\n " );
609+ msg .append (" • Create or regenerate a PAT at: https://github.com/settings/tokens\n " );
610+ }
611+ msg .append (" • For public repos no scope is needed; for private repos enable the 'repo' scope." );
612+ break ;
613+ case 403 :
614+ if (bodyMessage != null && bodyMessage .toLowerCase ().contains ("rate limit" )) {
615+ msg .append ("FIX: GitHub API rate limit exceeded (60/hr unauthenticated).\n " );
616+ msg .append (" • Add a token to get 5,000 requests/hour: github { accessToken = \" ghp_YOUR_TOKEN\" } in build.gradle\n " );
617+ msg .append (" • Or wait and retry later." );
618+ } else {
619+ msg .append ("FIX: Request forbidden — token may lack permission.\n " );
620+ msg .append (" • For private repositories, ensure your PAT has the 'repo' scope.\n " );
621+ msg .append (" • Update token at: https://github.com/settings/tokens" );
622+ }
623+ break ;
624+ case 404 :
625+ msg .append ("FIX: Repository or release not found.\n " );
626+ msg .append (" • Check that the owner, repo name, and release tag are correct in your githubImplementation dependency.\n " );
627+ msg .append (" • If the repo is private, ensure your token has access to it." );
628+ break ;
629+ default :
630+ msg .append ("FIX: Check your network and GitHub status. Retry with --info for more details." );
631+ break ;
632+ }
633+ return msg .toString ();
634+ }
635+
636+ /**
637+ * Builds a user-friendly error message for a failed HTTP response when the body is not JSON
638+ * (e.g. asset download). Does not consume the response body.
639+ */
640+ private String buildHttpErrorMessage (int code , String statusMessage , String context ) {
641+ StringBuilder msg = new StringBuilder ();
642+ msg .append ("Download failed for " ).append (context ).append (". HTTP " ).append (code ).append (" " ).append (statusMessage ).append (".\n \n " );
643+ switch (code ) {
644+ case 401 :
645+ msg .append ("FIX: Add a token so the plugin can download the asset: github { accessToken = \" ghp_YOUR_TOKEN\" } in build.gradle, or set GITHUB_TOKEN." );
646+ break ;
647+ case 403 :
648+ msg .append ("FIX: If rate limited, add a token (github { accessToken = \" ...\" }). If forbidden, ensure your PAT has the 'repo' scope for this repository." );
649+ break ;
650+ case 404 :
651+ msg .append ("FIX: Check that the release and asset exist at the given tag. For private repos, ensure your token has access." );
652+ break ;
653+ default :
654+ break ;
655+ }
656+ return msg .toString ();
657+ }
658+
563659 /**
564660 * Downloads and caches a release asset JAR file from a GitHub repository.
565661 *
@@ -591,8 +687,12 @@ public File getAsset(String repoOwner, String repoName, String version) {
591687 logger .debug ("Fetching release from: " + apiUrl );
592688
593689 try (Response response = makeGitHubApiRequest (apiUrl )) {
594- if (!response .isSuccessful () || response .body () == null ) {
595- throw new RuntimeException ("Failed to fetch release: " + response .code () + " " + response .message ());
690+ if (!response .isSuccessful ()) {
691+ String context = String .format ("release %s/%s tag %s" , repoOwner , repoName , version );
692+ throw new RuntimeException (buildGitHubApiErrorMessage (response , context ));
693+ }
694+ if (response .body () == null ) {
695+ throw new RuntimeException ("GitHub API returned empty body for release " + repoOwner + "/" + repoName + " tag " + version + "." );
596696 }
597697
598698 JsonObject release = gson .fromJson (response .body ().string (), JsonObject .class );
@@ -663,8 +763,12 @@ private void downloadAssetFromUrl(File destination, String downloadUrl, String r
663763
664764 try (Response response = httpClient .newCall (request ).execute ()) {
665765 logger .debug ("HTTP response: " + response .code () + " " + response .message ());
666- if (!response .isSuccessful () || response .body () == null ) {
667- throw new IOException ("Failed to download asset: " + response );
766+ if (!response .isSuccessful ()) {
767+ String context = repoOwner + "/" + repoName ;
768+ throw new IOException (buildHttpErrorMessage (response .code (), response .message (), context ));
769+ }
770+ if (response .body () == null ) {
771+ throw new IOException ("Empty response body when downloading " + repoOwner + "/" + repoName + "." );
668772 }
669773 byte [] bytes = response .body ().bytes ();
670774 logger .debug ("Download size: " + bytes .length + " bytes." );
@@ -712,8 +816,12 @@ public JsonObject getLatestRelease(String repoOwner, String repoName) {
712816 return null ;
713817 }
714818
715- if (!response .isSuccessful () || response .body () == null ) {
716- throw new RuntimeException ("Failed to fetch latest release: " + response .code () + " " + response .message ());
819+ if (!response .isSuccessful ()) {
820+ String context = String .format ("latest release for %s/%s" , repoOwner , repoName );
821+ throw new RuntimeException (buildGitHubApiErrorMessage (response , context ));
822+ }
823+ if (response .body () == null ) {
824+ throw new RuntimeException ("GitHub API returned empty body for latest release " + repoOwner + "/" + repoName + "." );
717825 }
718826
719827 JsonObject release = gson .fromJson (response .body ().string (), JsonObject .class );
0 commit comments