diff --git a/src/main/java/org/kohsuke/github/GHGraphQLException.java b/src/main/java/org/kohsuke/github/GHGraphQLException.java
new file mode 100644
index 0000000000..b9b9a7f55a
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/GHGraphQLException.java
@@ -0,0 +1,101 @@
+package org.kohsuke.github;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import org.kohsuke.github.internal.graphql.response.GHGraphQLError;
+
+import java.util.Collections;
+import java.util.List;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+
+/**
+ * Thrown when a GitHub GraphQL request returns a successful HTTP response whose body contains a non-empty
+ * {@code errors} array.
+ *
+ *
+ * This exception preserves the structured error list so callers can branch on the error {@link GHGraphQLError#getType()
+ * type}, {@link GHGraphQLError#getPath() path}, and other fields. Partial response data is exposed through
+ * {@link #getResponseData()} and the originating query through {@link #getQuery()} to ease debugging.
+ *
+ *
+ *
+ * HTTP-level failures (4xx/5xx) do not surface as this exception; they continue to be reported as {@link HttpException}
+ * or its subclasses, since the response cannot be parsed as a GraphQL payload.
+ *
+ *
+ * @see GraphQL spec — Errors
+ */
+public class GHGraphQLException extends GHIOException {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Structured GraphQL errors. Marked transient because {@link GHGraphQLError} carries Jackson-populated
+ * {@code Object} fields that may not be {@link java.io.Serializable}.
+ */
+ private final transient List errors;
+
+ /** Original GraphQL query, when available; useful for debugging. */
+ private final String query;
+
+ /** Partial response data, transient because the payload type is unconstrained. */
+ private final transient Object responseData;
+
+ /**
+ * Instantiates a new GraphQL exception.
+ *
+ * @param message
+ * human-readable summary suitable for log output
+ * @param errors
+ * the structured GraphQL errors returned by the server, never {@code null}
+ * @param responseData
+ * the partial {@code data} payload, or {@code null} if the server returned none
+ * @param query
+ * the GraphQL query string sent in the originating request, or {@code null} if unknown
+ */
+ @SuppressFBWarnings(value = { "EI_EXPOSE_REP2" }, justification = "errors list is wrapped unmodifiable")
+ public GHGraphQLException(@Nonnull String message,
+ @Nonnull List errors,
+ @CheckForNull Object responseData,
+ @CheckForNull String query) {
+ super(message);
+ this.errors = Collections.unmodifiableList(errors);
+ this.responseData = responseData;
+ this.query = query;
+ }
+
+ /**
+ * Get the structured error entries returned by the GraphQL endpoint. The list is unmodifiable. Returns an empty
+ * list after this exception has been deserialized across a JVM boundary, since the structured errors are not
+ * preserved through serialization.
+ *
+ * @return the GraphQL errors
+ */
+ @Nonnull
+ public List getErrors() {
+ return errors == null ? Collections.emptyList() : errors;
+ }
+
+ /**
+ * Get the GraphQL query that produced this response, when available.
+ *
+ * @return the query string, or {@code null}
+ */
+ @CheckForNull
+ public String getQuery() {
+ return query;
+ }
+
+ /**
+ * Get the partial response data returned alongside the errors, if any. GraphQL allows servers to return both
+ * {@code data} and {@code errors} when a request only partially fails. Returns {@code null} when the server
+ * returned no data or after deserialization, e.g. across the network.
+ *
+ * @return the partial response data, or {@code null}
+ */
+ @CheckForNull
+ public Object getResponseData() {
+ return responseData;
+ }
+}
diff --git a/src/main/java/org/kohsuke/github/Requester.java b/src/main/java/org/kohsuke/github/Requester.java
index 95f0366ebd..67095eca90 100644
--- a/src/main/java/org/kohsuke/github/Requester.java
+++ b/src/main/java/org/kohsuke/github/Requester.java
@@ -102,6 +102,9 @@ public T fetch(@Nonnull Class type) throws IOException {
* @param type
* the type
* @return an instance of {@code GHGraphQLResponse}
+ * @throws GHGraphQLException
+ * if the server returns a successful HTTP response whose body contains a non-empty {@code errors}
+ * array.
* @throws IOException
* if the server returns 4xx/5xx responses.
*/
@@ -109,7 +112,11 @@ public , S> S fetchGraphQL(@Nonnull Class type
T response = fetch(type);
if (!response.isSuccessful()) {
- throw new IOException("GraphQL request failed by:" + response.getErrorMessages());
+ String message = response.buildErrorSummary("Request failed due to following response errors");
+ throw new GHGraphQLException(message,
+ response.getErrors(),
+ response.getDataUnchecked(),
+ extractGraphQLQuery());
}
return response.getData();
@@ -201,4 +208,19 @@ public PagedIterable toIterable(Class type, Consumer itemInitiali
return new GitHubPageContentsIterable<>(client, build(), type, itemInitializer);
}
+
+ /**
+ * Best-effort lookup of the {@code query} parameter set by {@link GitHub#createGraphQLRequest(String)}. Returns
+ * {@code null} if the parameter is missing, e.g. when the request was assembled by hand.
+ *
+ * @return the query string sent in this request, or {@code null}
+ */
+ private String extractGraphQLQuery() {
+ for (GitHubRequest.Entry entry : build().args()) {
+ if ("query".equals(entry.key) && entry.value instanceof String) {
+ return (String) entry.value;
+ }
+ }
+ return null;
+ }
}
diff --git a/src/main/java/org/kohsuke/github/internal/graphql/response/GHGraphQLError.java b/src/main/java/org/kohsuke/github/internal/graphql/response/GHGraphQLError.java
new file mode 100644
index 0000000000..7360addb6c
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/internal/graphql/response/GHGraphQLError.java
@@ -0,0 +1,135 @@
+package org.kohsuke.github.internal.graphql.response;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.CheckForNull;
+
+/**
+ * A single error entry returned by a GitHub GraphQL response.
+ *
+ *
+ * Per the GraphQL specification, only {@code message} is guaranteed to be present. Other fields ({@code type},
+ * {@code path}, {@code locations}, {@code extensions}) are optional and may be {@code null} depending on the error.
+ * Unknown fields are ignored to remain forward-compatible with future server-side additions.
+ *
+ *
+ * @see GraphQL spec — Errors
+ * @see GitHub GraphQL — Error
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@SuppressFBWarnings(value = { "UWF_UNWRITTEN_FIELD", "UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR" },
+ justification = "Populated via Jackson deserialization")
+public class GHGraphQLError {
+
+ /**
+ * Source location of an error inside the GraphQL document.
+ */
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ @SuppressFBWarnings(value = { "UWF_UNWRITTEN_FIELD", "UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR" },
+ justification = "Populated via Jackson deserialization")
+ public static class Location {
+
+ private int column;
+
+ private int line;
+
+ /**
+ * Default constructor used by Jackson.
+ */
+ public Location() {
+ }
+
+ /**
+ * Get the column index of the location, starting at 1.
+ *
+ * @return the column index
+ */
+ public int getColumn() {
+ return column;
+ }
+
+ /**
+ * Get the line index of the location, starting at 1.
+ *
+ * @return the line index
+ */
+ public int getLine() {
+ return line;
+ }
+ }
+
+ private Map extensions;
+
+ private List locations;
+
+ private String message;
+
+ private List