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

Cole-Greer pushed a commit to branch GLVBehaviouralAlignment
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit 00b7d21f53ac61871077b77e220f57257f219b71
Author: Cole Greer <[email protected]>
AuthorDate: Tue Jun 2 12:37:56 2026 -0700

    Improve empty-response error and add request-encoding tests (gremlin-driver)
    
    - Produce a clear "Server returned an empty response body" error instead of 
a
      bare EOFException when the response body is empty (tinkerpop-28f).
    - Add hermetic HttpGremlinRequestEncoder tests validating the User-Agent 
header
      (present/absent) and that per-request settings (timeout, batchSize,
      materializeProperties) survive encoding into the outgoing request
      (tinkerpop-3tw.8).
---
 .../stream/GraphBinaryStreamResponseReader.java    |  19 ++-
 .../GraphBinaryStreamResponseReaderTest.java       |  23 ++++
 .../handler/HttpGremlinRequestEncoderTest.java     | 133 +++++++++++++++++++++
 3 files changed, 173 insertions(+), 2 deletions(-)

diff --git 
a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/stream/GraphBinaryStreamResponseReader.java
 
b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/stream/GraphBinaryStreamResponseReader.java
index 11491903e8..ba7370f71e 100644
--- 
a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/stream/GraphBinaryStreamResponseReader.java
+++ 
b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/stream/GraphBinaryStreamResponseReader.java
@@ -29,6 +29,7 @@ import 
org.apache.tinkerpop.gremlin.structure.io.binary.Marker;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.EOFException;
 import java.util.concurrent.atomic.AtomicReference;
 
 /**
@@ -95,11 +96,25 @@ public class GraphBinaryStreamResponseReader implements 
Runnable {
                 resultSet.markError(new 
ResponseException(HttpResponseStatus.valueOf(statusCode), message, exception));
             }
         } catch (Throwable t) {
-            logger.warn("Error reading streaming response", t);
-            resultSet.markError(t);
+            if (buffer.readerIndex() == 0 && hasCause(t, EOFException.class)) {
+                final RuntimeException empty = new RuntimeException(
+                        "Server returned an empty response body", t);
+                logger.warn("Error reading streaming response", empty);
+                resultSet.markError(empty);
+            } else {
+                logger.warn("Error reading streaming response", t);
+                resultSet.markError(t);
+            }
         } finally {
             pendingResultSet.compareAndSet(resultSet, null);
             buffer.release();
         }
     }
+
+    private static boolean hasCause(final Throwable t, final Class<? extends 
Throwable> type) {
+        for (Throwable cause = t; cause != null; cause = cause.getCause()) {
+            if (type.isInstance(cause)) return true;
+        }
+        return false;
+    }
 }
diff --git 
a/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/handler/GraphBinaryStreamResponseReaderTest.java
 
b/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/handler/GraphBinaryStreamResponseReaderTest.java
index 484ec7d524..2b657a1f9b 100644
--- 
a/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/handler/GraphBinaryStreamResponseReaderTest.java
+++ 
b/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/handler/GraphBinaryStreamResponseReaderTest.java
@@ -223,4 +223,27 @@ public class GraphBinaryStreamResponseReaderTest {
         assertTrue(results.isEmpty());
         assertNull(pending.get());
     }
+
+    @Test
+    public void shouldProduceClearErrorOnEmptyBody() throws Exception {
+        final ResultSet rs = new ResultSet(executor, 
RequestMessage.build("g.V()").create(), null);
+        final AtomicReference<ResultSet> pending = new AtomicReference<>(rs);
+
+        // Simulate an empty HTTP response body — no bytes offered before 
end-of-stream
+        final ByteBufQueueInputStream stream = new ByteBufQueueInputStream();
+        stream.signalEndOfStream();
+
+        final InputStreamBuffer buffer = new InputStreamBuffer(stream);
+        new GraphBinaryStreamResponseReader(buffer, reader, rs, pending).run();
+
+        assertTrue(rs.allItemsAvailable());
+        try {
+            rs.all().get();
+            fail("Expected exception");
+        } catch (Exception e) {
+            assertTrue(e.getCause() instanceof RuntimeException);
+            assertEquals("Server returned an empty response body", 
e.getCause().getMessage());
+        }
+        assertNull(pending.get());
+    }
 }
diff --git 
a/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/handler/HttpGremlinRequestEncoderTest.java
 
b/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/handler/HttpGremlinRequestEncoderTest.java
new file mode 100644
index 0000000000..b822bbcab8
--- /dev/null
+++ 
b/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/handler/HttpGremlinRequestEncoderTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.driver.handler;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.embedded.EmbeddedChannel;
+import io.netty.handler.codec.http.FullHttpRequest;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.tinkerpop.gremlin.driver.RequestInterceptor;
+import org.apache.tinkerpop.gremlin.driver.RequestOptions;
+import org.apache.tinkerpop.gremlin.driver.UserAgent;
+import 
org.apache.tinkerpop.gremlin.driver.interceptor.PayloadSerializingInterceptor;
+import org.apache.tinkerpop.gremlin.util.Tokens;
+import org.apache.tinkerpop.gremlin.util.message.RequestMessage;
+import org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV4;
+import org.junit.Test;
+
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.net.URI;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public class HttpGremlinRequestEncoderTest {
+
+    private static final InetSocketAddress REMOTE = new 
InetSocketAddress("127.0.0.1", 8182);
+
+    private static class TestChannel extends EmbeddedChannel {
+        TestChannel(ChannelHandler... handlers) {
+            super(handlers);
+        }
+
+        @Override
+        public SocketAddress remoteAddress() {
+            return REMOTE;
+        }
+    }
+
+    private EmbeddedChannel createChannel(final boolean userAgentEnabled) {
+        final GraphBinaryMessageSerializerV4 serializer = new 
GraphBinaryMessageSerializerV4();
+        final List<Pair<String, ? extends RequestInterceptor>> interceptors =
+                Collections.singletonList(Pair.of("serializer", new 
PayloadSerializingInterceptor(serializer)));
+        final URI uri = URI.create("http://localhost:8182/gremlin";);
+        final HttpGremlinRequestEncoder encoder = new 
HttpGremlinRequestEncoder(
+                serializer, interceptors, userAgentEnabled, false, uri);
+        return new TestChannel(encoder);
+    }
+
+    @Test
+    public void shouldIncludeUserAgentHeader() {
+        final EmbeddedChannel channel = createChannel(true);
+        final RequestMessage msg = RequestMessage.build("g.V()").create();
+
+        assertTrue(channel.writeOutbound(msg));
+        final FullHttpRequest request = channel.readOutbound();
+
+        assertNotNull(request);
+        assertEquals(UserAgent.USER_AGENT, 
request.headers().get("user-agent"));
+        request.release();
+    }
+
+    @Test
+    public void shouldExcludeUserAgentHeaderWhenDisabled() {
+        final EmbeddedChannel channel = createChannel(false);
+        final RequestMessage msg = RequestMessage.build("g.V()").create();
+
+        assertTrue(channel.writeOutbound(msg));
+        final FullHttpRequest request = channel.readOutbound();
+
+        assertNotNull(request);
+        assertNull(request.headers().get("user-agent"));
+        request.release();
+    }
+
+    @Test
+    public void shouldEncodePerRequestSettingsInBody() throws Exception {
+        final EmbeddedChannel channel = createChannel(true);
+        final GraphBinaryMessageSerializerV4 deserializer = new 
GraphBinaryMessageSerializerV4();
+
+        // Build RequestMessage the same way Client.submitAsync does from 
RequestOptions
+        final RequestOptions options = RequestOptions.build()
+                .timeout(5000L)
+                .batchSize(250)
+                .materializeProperties(Tokens.MATERIALIZE_PROPERTIES_TOKENS)
+                .create();
+
+        final RequestMessage.Builder builder = RequestMessage.build("g.V()");
+        builder.addChunkSize(options.getBatchSize().get());
+        options.getTimeout().ifPresent(builder::addTimeoutMillis);
+        
options.getMaterializeProperties().ifPresent(builder::addMaterializeProperties);
+        final RequestMessage msg = builder.create();
+
+        assertTrue(channel.writeOutbound(msg));
+        final FullHttpRequest request = channel.readOutbound();
+        assertNotNull(request);
+
+        try {
+            // Deserialize the body back into a RequestMessage to prove 
round-trip fidelity
+            final ByteBuf body = request.content();
+            assertFalse("body should not be empty", body.readableBytes() == 0);
+            final RequestMessage decoded = 
deserializer.deserializeBinaryRequest(body);
+
+            assertEquals(5000L, (long) decoded.getField(Tokens.TIMEOUT_MS));
+            assertEquals(250, (int) decoded.getField(Tokens.ARGS_BATCH_SIZE));
+            assertEquals(Tokens.MATERIALIZE_PROPERTIES_TOKENS, 
decoded.getField(Tokens.ARGS_MATERIALIZE_PROPERTIES));
+        } finally {
+            request.release();
+        }
+    }
+}

Reply via email to