Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions src/main/java/org/kohsuke/github/GHGraphQLException.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>
* 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.
* </p>
*
* <p>
* 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.
* </p>
*
* @see <a href="https://spec.graphql.org/October2021/#sec-Errors">GraphQL spec — Errors</a>
*/
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<GHGraphQLError> 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<GHGraphQLError> 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<GHGraphQLError> 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;
}
}
24 changes: 23 additions & 1 deletion src/main/java/org/kohsuke/github/Requester.java
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,21 @@ public <T> T fetch(@Nonnull Class<T> type) throws IOException {
* @param type
* the type
* @return an instance of {@code GHGraphQLResponse<T>}
* @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.
*/
public <T extends GHGraphQLResponse<S>, S> S fetchGraphQL(@Nonnull Class<T> type) throws IOException {
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();
Expand Down Expand Up @@ -201,4 +208,19 @@ public <R> PagedIterable<R> toIterable(Class<R[]> type, Consumer<R> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>
* 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.
* </p>
*
* @see <a href="https://spec.graphql.org/October2021/#sec-Errors">GraphQL spec — Errors</a>
* @see <a href="https://docs.github.com/en/graphql/reference/objects#error">GitHub GraphQL — Error</a>
*/
@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<String, Object> extensions;

private List<Location> locations;

private String message;

private List<Object> path;

private String type;

/**
* Default constructor used by Jackson.
*/
public GHGraphQLError() {
}

/**
* Get the extensions object as defined by the GraphQL spec. GitHub may include custom keys here, e.g. error code or
* documentation URL. May be {@code null} if not provided.
*
* @return the extensions map, or {@code null}
*/
@CheckForNull
public Map<String, Object> getExtensions() {
return extensions == null ? null : Collections.unmodifiableMap(extensions);
}

/**
* Get the source locations associated with this error. Each entry points to a line/column in the GraphQL document.
* May be {@code null} if not provided.
*
* @return the list of locations, or {@code null}
*/
@CheckForNull
public List<Location> getLocations() {
return locations == null ? null : Collections.unmodifiableList(locations);
}

/**
* Get the human-readable error message. Per the GraphQL spec this field is always present.
*
* @return the message
*/
public String getMessage() {
return message;
}

/**
* Get the response path that failed. Each segment is either a {@link String} field name or an {@link Integer} list
* index, per the GraphQL spec. May be {@code null} if not provided.
*
* @return the path elements, or {@code null}
*/
@CheckForNull
public List<Object> getPath() {
return path == null ? null : Collections.unmodifiableList(path);
}

/**
* Get the error type as classified by the server. GitHub commonly uses values such as {@code FORBIDDEN},
* {@code NOT_FOUND}, {@code RATE_LIMITED}, or {@code UNPROCESSABLE}. This is a GitHub extension to the GraphQL spec
* and may be {@code null}.
*
* @return the error type, or {@code null}
*/
@CheckForNull
public String getType() {
return type;
}
}
Loading