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); + } + } +}
