Skip to content
Draft
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
2 changes: 2 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
87 changes: 5 additions & 82 deletions packages/graph-explorer-proxy-server/src/node-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("<html><body>500 Internal Server Error</body></html>");
});

// POST endpoint for openCypher queries.
Expand Down
4 changes: 2 additions & 2 deletions packages/graph-explorer/src/components/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ const DialogContent = React.forwardRef<
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<div className="fixed inset-0 z-50 flex h-full w-full items-center justify-center p-20">
<div className="fixed inset-0 z-50 flex h-full w-full items-center justify-center p-4 sm:p-20">
<DialogPrimitive.Content
ref={ref}
className={cn(
"bg-background-default data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 relative grid max-h-full max-w-lg overflow-y-auto duration-200 sm:rounded-lg",
"bg-background-default data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 relative grid max-h-full w-full min-w-0 max-w-lg overflow-y-auto rounded-lg duration-200 sm:min-w-[425px]",
className
)}
{...props}
Expand Down
122 changes: 122 additions & 0 deletions packages/graph-explorer/src/connector/fetchDatabaseRequest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { decodeErrorSafely } from "./fetchDatabaseRequest";

describe("decodeErrorSafely", () => {
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("<html></html>", {
status: 500,
headers: {
"Content-Type": "text/html",
},
});

const decoded = await decodeErrorSafely(response);

expect(decoded).toEqual({
status: 500,
contentType: "text/html",
content: "text",
text: "<html></html>",
});
});

it("should decode xml responses", async () => {
const response = new Response("<xml></xml>", {
status: 500,
headers: {
"Content-Type": "text/xml",
},
});

const decoded = await decodeErrorSafely(response);

expect(decoded).toEqual({
status: 500,
contentType: "text/xml",
content: "text",
text: "<xml></xml>",
});
});

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",
});
});
});
Loading