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

commit f5880e73e33abf27ae6a2f6a709a099efd674cc0
Author: Ken Hu <[email protected]>
AuthorDate: Fri Jun 5 15:34:47 2026 -0700

    Improve GremlinServer HTTP pipelining behavior CTR.
    
    Silently ignoring pipelined requests would cause clients to wait
    for a response that would never come. While the documentation did
    state that pipelining isn't supported, this change makes the
    server close the connection in the event of a pipelined request
    after the first one completes. This gives a signal to the client
    that the pipelined request won't be responded to.
    
    Assisted-by: Claude Code:claude-opus-4-6
---
 docs/src/reference/gremlin-applications.asciidoc   |  6 +-
 .../server/handler/HttpRequestIdHandler.java       | 10 ++-
 .../GremlinServerHttpPipeliningIntegrateTest.java  | 72 ++++++++++++++++++++++
 3 files changed, 85 insertions(+), 3 deletions(-)

diff --git a/docs/src/reference/gremlin-applications.asciidoc 
b/docs/src/reference/gremlin-applications.asciidoc
index 18391c5506..6494972cdf 100644
--- a/docs/src/reference/gremlin-applications.asciidoc
+++ b/docs/src/reference/gremlin-applications.asciidoc
@@ -821,8 +821,10 @@ Gremlin script.  The caveat is that these arguments will 
always be treated as `S
 types are preserved or to pass complex objects such as lists or maps, use 
`POST` which will at least support the
 allowed JSON data types.
 
-NOTE: The Gremlin Server doesn't support 
link:https://en.wikipedia.org/wiki/HTTP_pipelining[HTTP pipelining]. Attempts
-to use this feature will cause the server to throw an error and may lead to 
results being sent out-of-order.
+NOTE: The Gremlin Server does not support 
link:https://en.wikipedia.org/wiki/HTTP_pipelining[HTTP pipelining]. Sending
+a second request on the same connection before the first response completes 
will cause the server to close the
+connection after the current response finishes. Clients must wait for a 
response before sending the next request on a
+keep-alive connection.
 
 Passing the `Accept` header with a valid MIME type will trigger the server to 
return the result in a particular format.
 Note that in addition to the formats available given the server's 
`serializers` configuration, there is also a basic
diff --git 
a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpRequestIdHandler.java
 
b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpRequestIdHandler.java
index 6741d0aaab..55b0e9d71c 100644
--- 
a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpRequestIdHandler.java
+++ 
b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpRequestIdHandler.java
@@ -40,6 +40,8 @@ public class HttpRequestIdHandler extends 
ChannelDuplexHandler {
      */
     private static final AttributeKey<Boolean> IN_USE = 
AttributeKey.valueOf("inUse");
 
+    private static final AttributeKey<Boolean> CLOSE_AFTER_RESPONSE = 
AttributeKey.valueOf("closeAfterResponse");
+
     public static String REQUEST_ID_HEADER_NAME = "Gremlin-RequestId";
 
     @Override
@@ -47,7 +49,8 @@ public class HttpRequestIdHandler extends 
ChannelDuplexHandler {
         if (msg instanceof HttpRequest) {
             final Boolean currentlyInUse = ctx.channel().attr(IN_USE).get();
             if (currentlyInUse != null && currentlyInUse == true) {
-                // Pipelining not supported so just ignore the request if 
another request already being handled.
+                // Pipelining not supported — mark connection for close after 
current response completes.
+                ctx.channel().attr(CLOSE_AFTER_RESPONSE).set(true);
                 ReferenceCountUtil.release(msg);
                 return;
             }
@@ -73,6 +76,11 @@ public class HttpRequestIdHandler extends 
ChannelDuplexHandler {
         }
         if (msg instanceof LastHttpContent) { // possible for an object to be 
both HttpResponse and LastHttpContent.
             ctx.channel().attr(IN_USE).set(false);
+
+            final Boolean closeAfter = 
ctx.channel().attr(CLOSE_AFTER_RESPONSE).get();
+            if (closeAfter != null && closeAfter) {
+                promise.addListener(future -> ctx.close());
+            }
         }
 
         ctx.write(msg, promise);
diff --git 
a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpPipeliningIntegrateTest.java
 
b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpPipeliningIntegrateTest.java
new file mode 100644
index 0000000000..aed9d37f20
--- /dev/null
+++ 
b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpPipeliningIntegrateTest.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tinkerpop.gremlin.server;
+
+import org.apache.tinkerpop.gremlin.server.channel.HttpChannelizer;
+import org.junit.Test;
+
+import java.io.InputStream;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.Assert.assertEquals;
+
+public class GremlinServerHttpPipeliningIntegrateTest extends 
AbstractGremlinServerIntegrationTest {
+
+    @Override
+    public Settings overrideSettings(final Settings settings) {
+        settings.channelizer = HttpChannelizer.class.getName();
+        return settings;
+    }
+
+    @Test
+    public void shouldCloseConnectionOnPipelinedRequest() throws Exception {
+        final String request = "POST /gremlin HTTP/1.1\r\n" +
+                "Host: localhost:" + TestClientFactory.PORT + "\r\n" +
+                "Content-Type: application/json\r\n" +
+                "Content-Length: 27\r\n" +
+                "Connection: keep-alive\r\n\r\n" +
+                "{\"gremlin\": \"g.inject(1)\"}";
+
+        // send two requests in a single write so both arrive before the first 
can complete
+        final byte[] pipelined = (request + 
request).getBytes(StandardCharsets.UTF_8);
+
+        try (final Socket socket = new Socket("localhost", 
TestClientFactory.PORT)) {
+            socket.setSoTimeout(10000);
+            socket.getOutputStream().write(pipelined);
+            socket.getOutputStream().flush();
+
+            final InputStream in = socket.getInputStream();
+            final byte[] buf = new byte[8192];
+            final StringBuilder sb = new StringBuilder();
+
+            // read until the server closes the connection
+            int bytesRead;
+            while ((bytesRead = in.read(buf)) != -1) {
+                sb.append(new String(buf, 0, bytesRead, 
StandardCharsets.UTF_8));
+            }
+
+            final String fullResponse = sb.toString();
+
+            // should contain exactly one HTTP response (the first request's) 
and then close
+            final int responseCount = fullResponse.split("HTTP/1.1 200 
OK").length - 1;
+            assertEquals("Expected exactly one response before connection 
close", 1, responseCount);
+        }
+    }
+}

Reply via email to