diff --git a/Changelog.md b/Changelog.md
index b4a5adfa8..d48e2459e 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -66,6 +66,8 @@ And as always, there are many additional small fixes and improvements.
([#839](https://github.com/aws/graph-explorer/pull/839))
- **Fixed** proxy server error when a request is aborted mid-stream
([#873](https://github.com/aws/graph-explorer/pull/873))
+- **Fixed** issue rendering error responses that include HTML
+ ([#905](https://github.com/aws/graph-explorer/pull/905))
- **Updated** icons in context menu to be more consistent
([#906](https://github.com/aws/graph-explorer/pull/906))
- **Updated** styling of buttons, input, textarea, and select fields
diff --git a/packages/graph-explorer-proxy-server/src/node-server.ts b/packages/graph-explorer-proxy-server/src/node-server.ts
index a3fecdd38..5cfdbb158 100644
--- a/packages/graph-explorer-proxy-server/src/node-server.ts
+++ b/packages/graph-explorer-proxy-server/src/node-server.ts
@@ -280,88 +280,11 @@ app.post("/sparql", (req, res, next) => {
});
// POST endpoint for Gremlin queries.
-app.post("/gremlin", (req, res, next) => {
- // Gather info from the headers
- const headers = req.headers as DbQueryIncomingHttpHeaders;
- const queryId = headers["queryid"];
- const graphDbConnectionUrl = headers["graph-db-connection-url"];
- const shouldLogDbQuery = BooleanStringSchema.default("false").parse(
- headers["db-query-logging-enabled"]
- );
- const isIamEnabled = !!headers["aws-neptune-region"];
- const region = isIamEnabled ? headers["aws-neptune-region"] : "";
- const serviceType = isIamEnabled
- ? (headers["service-type"] ?? DEFAULT_SERVICE_TYPE)
- : "";
-
- // Validate the input before making any external calls.
- const queryString = req.body.query;
- if (!queryString) {
- return res
- .status(400)
- .send({ error: "[Proxy] Gremlin: query not provided" });
- }
-
- if (shouldLogDbQuery) {
- proxyLogger.debug("[Gremlin] Received database query:\n%s", queryString);
- }
-
- /// Function to cancel long running queries if the client disappears before completion
- async function cancelQuery() {
- if (!queryId) {
- return;
- }
- proxyLogger.debug(`Cancelling request ${queryId}...`);
- try {
- await retryFetch(
- new URL(
- `${graphDbConnectionUrl}/gremlin/status?cancelQuery&queryId=${encodeURIComponent(queryId)}`
- ),
- { method: "GET" },
- isIamEnabled,
- region,
- serviceType
- );
- } catch (err) {
- // Not really an error
- proxyLogger.warn("Failed to cancel the query: %o", err);
- }
- }
-
- // Watch for a cancelled or aborted connection
- req.on("close", async () => {
- if (req.complete) {
- return;
- }
- await cancelQuery();
- });
- res.on("close", async () => {
- if (res.writableFinished) {
- return;
- }
- await cancelQuery();
- });
-
- const body = { gremlin: queryString, queryId };
- const rawUrl = `${graphDbConnectionUrl}/gremlin`;
- const requestOptions = {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Accept: "application/vnd.gremlin-v3.0+json",
- },
- body: JSON.stringify(body),
- };
-
- return fetchData(
- res,
- next,
- rawUrl,
- requestOptions,
- isIamEnabled,
- region,
- serviceType
- );
+app.post("/gremlin", (_req, res, _next) => {
+ // Respond with a 500 and html content
+ return res
+ .status(500)
+ .send("
500 Internal Server Error");
});
// POST endpoint for openCypher queries.
diff --git a/packages/graph-explorer/src/components/Dialog.tsx b/packages/graph-explorer/src/components/Dialog.tsx
index 60e59d6ec..739cb9a4d 100644
--- a/packages/graph-explorer/src/components/Dialog.tsx
+++ b/packages/graph-explorer/src/components/Dialog.tsx
@@ -32,11 +32,11 @@ const DialogContent = React.forwardRef<
>(({ className, children, ...props }, ref) => (
-
+
{
+ it("should decode text responses", async () => {
+ const response = new Response("Some error message", {
+ status: 500,
+ headers: {
+ "Content-Type": "text/plain",
+ },
+ });
+
+ const decoded = await decodeErrorSafely(response);
+
+ expect(decoded).toEqual({
+ status: 500,
+ contentType: "text/plain",
+ content: "text",
+ text: "Some error message",
+ });
+ });
+
+ it("should decode HTML responses", async () => {
+ const response = new Response("", {
+ status: 500,
+ headers: {
+ "Content-Type": "text/html",
+ },
+ });
+
+ const decoded = await decodeErrorSafely(response);
+
+ expect(decoded).toEqual({
+ status: 500,
+ contentType: "text/html",
+ content: "text",
+ text: "",
+ });
+ });
+
+ it("should decode xml responses", async () => {
+ const response = new Response("", {
+ status: 500,
+ headers: {
+ "Content-Type": "text/xml",
+ },
+ });
+
+ const decoded = await decodeErrorSafely(response);
+
+ expect(decoded).toEqual({
+ status: 500,
+ contentType: "text/xml",
+ content: "text",
+ text: "",
+ });
+ });
+
+ it("should return the error message inside the JSON response", async () => {
+ const response = new Response(
+ JSON.stringify({
+ message: "An error occurred",
+ }),
+ {
+ status: 500,
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const decoded = await decodeErrorSafely(response);
+
+ expect(decoded).toEqual({
+ status: 500,
+ contentType: "application/json",
+ content: "data",
+ data: { message: "An error occurred" },
+ });
+ });
+
+ it("should return the flattened error message inside the JSON response", async () => {
+ const response = new Response(
+ JSON.stringify({
+ error: { message: "An error occurred" },
+ }),
+ {
+ status: 500,
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const decoded = await decodeErrorSafely(response);
+
+ expect(decoded).toEqual({
+ status: 500,
+ contentType: "application/json",
+ content: "data",
+ data: {
+ message: "An error occurred",
+ },
+ });
+ });
+
+ it("should return undefined when the error contains no content", async () => {
+ const response = new Response(null, {
+ status: 500,
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const decoded = await decodeErrorSafely(response);
+
+ expect(decoded).toEqual({
+ status: 500,
+ contentType: "application/json",
+ content: "empty",
+ });
+ });
+});
diff --git a/packages/graph-explorer/src/connector/fetchDatabaseRequest.ts b/packages/graph-explorer/src/connector/fetchDatabaseRequest.ts
index 8f421822f..55326fdb3 100644
--- a/packages/graph-explorer/src/connector/fetchDatabaseRequest.ts
+++ b/packages/graph-explorer/src/connector/fetchDatabaseRequest.ts
@@ -15,44 +15,71 @@ const NeptuneErrorSchema = z.object({
type: z.string().optional(),
});
+interface ErrorDataResponse {
+ content: "data";
+ data: any;
+}
+
+interface ErrorTextResponse {
+ content: "text";
+ text: string;
+}
+
+interface ErrorEmptyResponse {
+ content: "empty";
+}
+
+type ErrorResponse = {
+ status: number;
+ contentType: string | null;
+} & (ErrorDataResponse | ErrorTextResponse | ErrorEmptyResponse);
+
/**
* Attempts to decode the error response into a JSON object.
*
* @param response The fetch response that should be decoded
* @returns The decoded response or undefined if it fails to decode
*/
-async function decodeErrorSafely(response: Response): Promise {
- const contentType = response.headers.get("Content-Type");
- const contentTypeHasValue = contentType !== null && contentType.length > 0;
- // Assume missing content type is JSON
- const isJson =
- !contentTypeHasValue || contentType.includes("application/json");
+export async function decodeErrorSafely(
+ response: Response
+): Promise {
+ const contentType = response.headers.get("Content-Type") ?? null;
+ const status = response.status;
// Extract the raw text from the response
const rawText = await response.text();
-
- // Check for empty response
if (!rawText) {
- return undefined;
+ return {
+ status,
+ contentType,
+ content: "empty",
+ };
}
- if (isJson) {
+ // Attempt to parse JSON
+ if (contentType?.includes("application/json")) {
try {
// Try parsing the response as JSON
const data = JSON.parse(rawText);
// Flatten the error if it contains an error object
- return data?.error ?? data;
+ return {
+ status,
+ contentType,
+ content: "data",
+ data: data?.error ?? data,
+ };
} catch (error) {
- console.error("Failed to decode the error response as JSON", {
+ // Log the error and fallthrough to return the raw text
+ logger.warn("Failed to decode the error response as JSON", {
error,
rawText,
});
- return rawText;
}
}
- return { message: rawText };
+ // Just return the whole response as the error
+ return { status, contentType, content: "text", text: rawText };
}
// Construct the request headers based on the connection settings
@@ -111,6 +138,11 @@ export async function fetchDatabaseRequest(
// Log the error to the console always
logger.error(`Response status ${response.status} received:`, error);
+ if (error.content === "text") {
+ if (error.contentType?.includes("text/plain")) {
+ }
+ }
+
// Parse out neptune specific error messages
const parseNeptuneError = NeptuneErrorSchema.safeParse(error);
if (parseNeptuneError.success) {
@@ -129,3 +161,62 @@ export async function fetchDatabaseRequest(
const data = await response.json();
return data;
}
+
+function getNeptuneErrorMessage(data: unknown) {
+ // Parse out neptune specific error messages
+ const parseNeptuneError = NeptuneErrorSchema.safeParse(data);
+ if (!parseNeptuneError.success) {
+ return null;
+ }
+
+ // Prefer the detailed message if it exists
+ const message =
+ parseNeptuneError.data.detailedMessage ??
+ parseNeptuneError.data.message ??
+ null;
+ return message;
+}
+
+function getDataOrText(response: Response) {
+ const contentType = response.headers.get("Content-Type") ?? null;
+
+ const rawText = await response.text();
+ if (!rawText) {
+ return null;
+ }
+
+ if (contentType?.includes("application/json")) {
+ try {
+ // Try parsing the response as JSON
+ return JSON.parse(rawText);
+ } catch (error) {
+ // Ignore the error and return the raw text
+ return rawText;
+ }
+ }
+
+ return rawText
+}
+
+function createNetworkErrorFromResponse(response: Response) {
+ const error = decodeErrorSafely(response);
+ const contentType = response.headers.get("Content-Type") ?? null;
+ const textOrData =
+
+ // Log the error to the console always
+ logger.error(`Response status ${response.status} received:`, {
+ error,
+ contentType,
+ });
+
+ if (!contentType) {
+ throw new NetworkError("Network response was not OK", response.status, null);
+ }
+
+ if (contentType) {
+ throw new NetworkError(
+ "Network response was not OK",
+ response.status,
+ error
+ );
+}