This is an automated email from the ASF dual-hosted git repository.

kenhuuu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git


The following commit(s) were added to refs/heads/master by this push:
     new b2b2bcb0d1 Prevent transaction leak on failed begin CTR
b2b2bcb0d1 is described below

commit b2b2bcb0d1e5bbc525fb8cb74b6652d688f409fa
Author: Ken Hu <[email protected]>
AuthorDate: Thu Jul 16 11:34:17 2026 -0700

    Prevent transaction leak on failed begin CTR
    
    There's two related issues here which both cause transaction leaks.
    The first is that the ordering of checking for errors matters and
    transactions needs to be last or else a transaction could get
    opened and leaked since the response will actually be an error.
    The second is the transaction might not properly begin itself so
    it needs to be cleaned up.
    
    Assisted-by: Codex:gpt-5.5
---
 .../server/handler/HttpGremlinEndpointHandler.java | 72 ++++++++++++----------
 .../GremlinServerHttpTransactionIntegrateTest.java | 19 ++++++
 2 files changed, 57 insertions(+), 34 deletions(-)

diff --git 
a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpGremlinEndpointHandler.java
 
b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpGremlinEndpointHandler.java
index 14f4a4584a..13531f856d 100644
--- 
a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpGremlinEndpointHandler.java
+++ 
b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpGremlinEndpointHandler.java
@@ -213,6 +213,39 @@ public class HttpGremlinEndpointHandler extends 
SimpleChannelInboundHandler<Requ
                             requestMessage.getGremlin());
                 }
 
+                // Validate the request before any transaction lifecycle side 
effects.
+                final Map<String, Object> args = requestMessage.getFields();
+                final String language = args.containsKey(Tokens.ARGS_LANGUAGE) 
? (String) args.get(Tokens.ARGS_LANGUAGE) : "gremlin-lang";
+                if 
(gremlinExecutor.getScriptEngineManager().getEngineByName(language) == null) {
+                    throw new 
ProcessingException(GremlinError.scriptEngineNotAvailable(language));
+                }
+
+                // Guard against bad parameters while trying to parse 
string-based parameters into a Map<String, Object>
+                if 
(requestMessage.optionalField(Tokens.ARGS_PARAMETERS).isPresent()) {
+                    Map<String, Object> parameters = null;
+                    final String parametersString = (String) 
requestMessage.getFields().get(Tokens.ARGS_PARAMETERS);
+                    try {
+                        parameters = 
GremlinQueryParser.parseParameters(parametersString);
+                    } catch (GremlinParserException e) {
+                        throw new 
ProcessingException(GremlinError.incorrectParameterFormat(parametersString, e));
+                    }
+
+                    if ("gremlin-groovy".equals(language)) {
+                        final Set<String> badParameters = 
IteratorUtils.set(IteratorUtils.<String>filter(
+                                parameters.keySet().iterator(),
+                                INVALID_PARAMETERS_KEYS::contains));
+                        if (!badParameters.isEmpty()) {
+                            throw new 
ProcessingException(GremlinError.parameter(badParameters));
+                        }
+                    }
+
+                    if (parameters.size() > settings.maxParameters) {
+                        throw new 
ProcessingException(GremlinError.parameter(parameters.size(), 
settings.maxParameters));
+                    }
+
+                    requestCtx.setParameters(parameters);
+                }
+
                 // These guards prevent any obvious failures from returning 
200 OK early by detecting them here and
                 // throwing before any other processing starts so the user 
gets a better error code.
                 final String txId = requestCtx.getTransactionId();
@@ -246,39 +279,6 @@ public class HttpGremlinEndpointHandler extends 
SimpleChannelInboundHandler<Requ
                     throw new 
ProcessingException(GremlinError.transactionalControlRequiresTransaction());
                 }
 
-                // validate script engine availability before committing the 
200 response
-                final Map<String, Object> args = requestMessage.getFields();
-                final String language = args.containsKey(Tokens.ARGS_LANGUAGE) 
? (String) args.get(Tokens.ARGS_LANGUAGE) : "gremlin-lang";
-                if 
(gremlinExecutor.getScriptEngineManager().getEngineByName(language) == null) {
-                    throw new 
ProcessingException(GremlinError.scriptEngineNotAvailable(language));
-                }
-
-                // Guard against bad parameters while trying to parse 
string-based parameters into a Map<String, Object>
-                if 
(requestMessage.optionalField(Tokens.ARGS_PARAMETERS).isPresent()) {
-                    Map<String, Object> parameters = null;
-                    final String parametersString = (String) 
requestMessage.getFields().get(Tokens.ARGS_PARAMETERS);
-                    try {
-                        parameters = 
GremlinQueryParser.parseParameters(parametersString);
-                    } catch (GremlinParserException e) {
-                        throw new 
ProcessingException(GremlinError.incorrectParameterFormat(parametersString, e));
-                    }
-
-                    if ("gremlin-groovy".equals(language)) {
-                        final Set<String> badParameters = 
IteratorUtils.set(IteratorUtils.<String>filter(
-                                parameters.keySet().iterator(),
-                                INVALID_PARAMETERS_KEYS::contains));
-                        if (!badParameters.isEmpty()) {
-                            throw new 
ProcessingException(GremlinError.parameter(badParameters));
-                        }
-                    }
-
-                    if (parameters.size() > settings.maxParameters) {
-                        throw new 
ProcessingException(GremlinError.parameter(parameters.size(), 
settings.maxParameters));
-                    }
-
-                    requestCtx.setParameters(parameters);
-                }
-
                 // Send back the 200 OK response header here since the 
response is always chunk transfer encoded. Any
                 // failures that follow this will show up in the response body 
instead.
                 coordinator.writeHeader(createResponseHeaders(ctx, serializer, 
requestCtx).toArray(CharSequence[]::new));
@@ -522,7 +522,8 @@ public class HttpGremlinEndpointHandler extends 
SimpleChannelInboundHandler<Requ
     private void doBegin(final Context ctx) throws Exception {
         final String traversalSourceName = 
ctx.getRequestMessage().getField(Tokens.ARGS_G);
 
-        final UnmanagedTransaction txCtx;
+        UnmanagedTransaction txCtx = null;
+        boolean closeTransactionOnFailure = true;
         try {
             txCtx = transactionManager.create(traversalSourceName);
             ctx.setTransactionId(txCtx.getTransactionId());
@@ -531,6 +532,7 @@ public class HttpGremlinEndpointHandler extends 
SimpleChannelInboundHandler<Requ
                 graph.tx().begin();
                 return null;
             }), ctx).get(5000, TimeUnit.MILLISECONDS); // Not an option for 
now, but 5s should be plenty.
+            closeTransactionOnFailure = false;
         } catch (IllegalStateException ise) {
             throw new 
ProcessingException(GremlinError.maxTransactionsExceeded(ise.getMessage()));
         } catch (IllegalArgumentException iae) {
@@ -539,6 +541,8 @@ public class HttpGremlinEndpointHandler extends 
SimpleChannelInboundHandler<Requ
             throw new 
ProcessingException(GremlinError.transactionNotSupported(uoe));
         } catch (ExecutionException | TimeoutException e) {
             throw new 
ProcessingException(GremlinError.transactionUnableToStart(e.getMessage()));
+        } finally {
+            if (closeTransactionOnFailure && txCtx != null) txCtx.close(false);
         }
     }
 
diff --git 
a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpTransactionIntegrateTest.java
 
b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpTransactionIntegrateTest.java
index c1def1e6ec..d507810478 100644
--- 
a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpTransactionIntegrateTest.java
+++ 
b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpTransactionIntegrateTest.java
@@ -353,6 +353,25 @@ public class GremlinServerHttpTransactionIntegrateTest 
extends AbstractGremlinSe
         }
     }
 
+    @Test
+    public void shouldNotCreateTransactionWhenBeginRequestValidationFails() 
throws Exception {
+        final TransactionManager txManager = 
server.getServerGremlinExecutor().getTransactionManager();
+
+        try (final CloseableHttpResponse r = postJson(client,
+                "{\"gremlin\":\"g.tx().begin()\",\"g\":\"" + GTX + 
"\",\"language\":\"not-an-engine\"}")) {
+            assertEquals(400, r.getStatusLine().getStatusCode());
+            assertTrue(extractStatusMessage(r).contains("Script engine 
[not-an-engine] is not available"));
+        }
+        assertEquals(0, txManager.getActiveTransactionCount());
+
+        try (final CloseableHttpResponse r = postJson(client,
+                "{\"gremlin\":\"g.tx().begin()\",\"g\":\"" + GTX + 
"\",\"parameters\":\"[x:]\"}")) {
+            assertEquals(400, r.getStatusLine().getStatusCode());
+            assertTrue(extractStatusMessage(r).contains("could not be 
converted into a Map"));
+        }
+        assertEquals(0, txManager.getActiveTransactionCount());
+    }
+
     @Test
     public void shouldTimeoutIdleTransactionWithNoOperations() throws 
Exception {
         final String txId = beginTx(client, GTX);

Reply via email to