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

spmallette pushed a commit to branch TINKERPOP-3238
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit 88b222949ce62a028afcbfa7d4354e5483750168
Author: Stephen Mallette <[email protected]>
AuthorDate: Mon Jun 15 16:25:27 2026 -0400

    TINKERPOP-3238 Throw FailResponseException for fail() step over remote
    
    The Java driver now throws a FailResponseException, a subclass of
    ResponseException that implements Failure, when the server returns a
    SERVER_ERROR_FAIL_STEP (595) status code. This makes remote handling of
    the fail() step more consistent with embedded behavior.
    
    Failure.format() is guarded against null traversal/traverser context
    since that data is not transmitted from the server, preventing a
    NullPointerException when formatting a remotely-reconstructed Failure.
    
    Adds unit tests for both the defensive format() logic and the new
    exception, plus documentation in the reference and upgrade guides.
    
    Assisted-by: Claude Code:claude-opus-4-8
---
 CHANGELOG.asciidoc                                 |   1 +
 docs/src/reference/the-traversal.asciidoc          |  14 +++
 docs/src/upgrade/release-3.7.x.asciidoc            |  17 ++++
 .../gremlin/process/traversal/Failure.java         | 107 ++++++++++++++-------
 .../gremlin/process/traversal/FailureTest.java     | 101 +++++++++++++++++++
 .../apache/tinkerpop/gremlin/driver/Handler.java   |  13 ++-
 .../driver/exception/FailResponseException.java    |  60 ++++++++++++
 .../exception/FailResponseExceptionTest.java       |  65 +++++++++++++
 .../gremlin/server/GremlinServerIntegrateTest.java |   4 +
 9 files changed, 346 insertions(+), 36 deletions(-)

diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index e07f7b64a1..8f37ba96ad 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -26,6 +26,7 @@ 
image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
 === TinkerPop 3.7.7 (Release Date: NOT OFFICIALLY RELEASED YET)
 
 * Added `NextN(n)` to `Traversal` in `gremlin-go` for batched result 
iteration, providing API parity with `next(n)` in the Java, Python, and .NET 
GLVs.
+* Added `FailResponseException` to `gremlin-driver` which is thrown `fail()` 
step is triggered on the server making it more consistent with embedded 
behavior.
 * Fixed conjoin has incorrect null handling.
 * Expanded `gremlin-python` CI matrix to test against Python 3.9, 3.10, 3.11, 
3.12, and 3.13.
 * Add Node 26 support for `gremlin-javascript` and `gremlint`.
diff --git a/docs/src/reference/the-traversal.asciidoc 
b/docs/src/reference/the-traversal.asciidoc
index 0715d31107..5876c36758 100644
--- a/docs/src/reference/the-traversal.asciidoc
+++ b/docs/src/reference/the-traversal.asciidoc
@@ -1667,6 +1667,20 @@ rollback. Moreover, the ability to rollback at all is 
graph provider dependent.
 configured without transaction support, will simply be left in a partially 
mutated state whether the action to rollback
 on `fail()` was implemented or not.
 
+The type of exception that `fail()` produces depends on how the traversal is 
executed:
+
+* *Embedded* - When executed directly against a `Graph` instance in the same 
JVM, `fail()` throws a
+`FailStep.FailException` which implements the `Failure` interface. The 
`Failure` provides programmatic access to the
+failure `Message`, `Traverser`, `Traversal` and `Metadata` as shown in the 
formatted output above.
+* *Remote (HTTP)* - When executed against Gremlin Server over HTTP, the server 
returns a `500` error with a message that
+indicates that the `fail()` step was triggered. The richer `Failure` data 
described above is not retained.
+* *Remote (WebSockets)* - When executed against Gremlin Server over 
WebSockets, the server returns the
+`595`/`SERVER_ERROR_FAIL_STEP` status code. In the Java driver this surfaces 
as a `FailResponseException`, a subclass of
+`ResponseException` that also implements the `Failure` interface so that the 
failure can be caught and handled in a
+manner more consistent with the embedded case. Note that the `Failure` data 
(e.g. `Traverser`, `Traversal`, `Metadata`)
+is not transmitted from the server, so those accessors return empty or `null` 
values. Other Gremlin Language Variants
+continue to surface this condition as their standard remote exception type.
+
 *Additional References*
 
 
link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#fail()++[`fail()`],
diff --git a/docs/src/upgrade/release-3.7.x.asciidoc 
b/docs/src/upgrade/release-3.7.x.asciidoc
index 70fea1d83e..0d7e175746 100644
--- a/docs/src/upgrade/release-3.7.x.asciidoc
+++ b/docs/src/upgrade/release-3.7.x.asciidoc
@@ -57,7 +57,24 @@ an empty string instead.
 
 See: link:https://issues.apache.org/jira/browse/TINKERPOP-3225[TINKERPOP-3225]
 
+==== FailResponseException for fail() Step
 
+The purpose of `fail()` step is to allow the user to raise error conditions 
when a traversal takes a particular path.
+It provides exception feedback to the user while immediately stopping the 
traversal in an error state. In embedded
+cases, it raises a `FailException` which implements the `Failure` interface 
and provides more details about the failure
+itself. Unfortunately, remote cases behaved differently and returned the 
common `ResponseException`. While it was
+possible to check the response code on this exception, it was not possible to 
catch the exception in quite the same way
+as embedded.
+
+To remedy this shortcoming, this version introduces a new 
`FailResponseException` which extends `ResponseException` and
+implements the `Failure` interface. When the driver encounters a 595 status 
code, an indicator that the `fail()` step
+was triggered on the server, it will throw a `FailResponseException` instead 
of the generic `ResponseException`. Note
+that the server does not send back many of the internal details needed to 
fully form a `Failure`, so the
+`FailResponseException` is really just a marker that makes it easier to trap 
these types of errors.
+
+Note that this change is for Java only and designed to better align embedded 
and remote use cases.
+
+See: link:https://issues.apache.org/jira/browse/TINKERPOP-3238[TINKERPOP-3238]
 
 == TinkerPop 3.7.6
 
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/Failure.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/Failure.java
index 775e685eea..3c5b2ed0e8 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/Failure.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/Failure.java
@@ -18,7 +18,7 @@
  */
 package org.apache.tinkerpop.gremlin.process.traversal;
 
-import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.FailStep;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
 import org.apache.tinkerpop.gremlin.process.traversal.step.util.EmptyStep;
 import 
org.apache.tinkerpop.gremlin.process.traversal.translator.GroovyTranslator;
 import 
org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement;
@@ -27,20 +27,45 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 
+/**
+ * Represents a failure raised by the {@link GraphTraversal#fail()} step which 
forces a traversal to immediately stop
+ * in an error state. Implementations carry the contextual information about 
where and why the failure occurred so that
+ * it can be inspected programmatically or rendered for display by way of 
{@link #format()}.
+ * <p/>
+ * When a {@code fail()} occurs in embedded mode the full context (the 
offending {@link Traverser} and {@link Traversal})
+ * is available. When reconstructed from a remote server response, that 
context is not transmitted and the relevant
+ * accessors may return {@code null}. Implementations and callers should 
account for that possibility.
+ *
+ * @author Stephen Mallette (http://stephen.genoprime.com)
+ */
 public interface Failure {
 
     static Translator.ScriptTranslator TRANSLATOR = GroovyTranslator.of("");
 
+    /**
+     * Gets the message associated with the failure as provided to the {@code 
fail()} step.
+     */
     String getMessage();
 
+    /**
+     * Gets any additional metadata associated with the failure. Returns an 
empty {@code Map} when there is no metadata.
+     */
     Map<String,Object> getMetadata();
 
+    /**
+     * Gets the {@link Traverser} that was being processed when the failure 
was triggered. May return {@code null} when
+     * the {@code Failure} was reconstructed from a remote response where this 
context is not available.
+     */
     Traverser.Admin getTraverser();
 
+    /**
+     * Gets the {@link Traversal} that contained the {@code fail()} step that 
triggered the failure. May return
+     * {@code null} when the {@code Failure} was reconstructed from a remote 
response where this context is not
+     * available.
+     */
     Traversal.Admin getTraversal();
 
     /**
@@ -48,44 +73,58 @@ public interface Failure {
      */
     public default String format() {
         final List<String> lines = new ArrayList<>();
-        final Step parentStep = (Step) getTraversal().getParent();
+
+        // some Failure implementations - notably those constructed from a 
remote response such as
+        // FailResponseException - do not carry the traversal/traverser 
context as that data is not transmitted
+        // from the server. guard against null returns so that format() 
degrades gracefully rather than throwing
+        // a NullPointerException.
+        final Traversal.Admin traversal = getTraversal();
+        final Traverser.Admin traverser = getTraverser();
+        final Step parentStep = traversal != null ? (Step) 
traversal.getParent() : null;
 
         lines.add(String.format("Message  > %s", getMessage()));
-        lines.add(String.format("Traverser> %s", getTraverser().toString()));
 
-        final TraverserGenerator generator = 
getTraversal().getTraverserGenerator();
-        final Traverser.Admin traverser = getTraverser();
-        if 
(generator.getProvidedRequirements().contains(TraverserRequirement.BULK)) {
-            lines.add(String.format("  Bulk   > %s", traverser.bulk()));
-        }
-        if 
(generator.getProvidedRequirements().contains(TraverserRequirement.SACK)) {
-            lines.add(String.format("  Sack   > %s", traverser.sack()));
-        }
-        if 
(generator.getProvidedRequirements().contains(TraverserRequirement.PATH)) {
-            lines.add(String.format("  Path   > %s", traverser.path()));
-        }
-        if 
(generator.getProvidedRequirements().contains(TraverserRequirement.SINGLE_LOOP) 
||
-                
generator.getProvidedRequirements().contains(TraverserRequirement.NESTED_LOOP) 
) {
-            final Set<String> loopNames = traverser.getLoopNames();
-            final String loopsLine = loopNames.isEmpty() ?
-                    String.valueOf(traverser.asAdmin().loops()) :
-                    loopNames.stream().collect(Collectors.toMap(loopName -> 
loopName, traverser::loops)).toString();
-            lines.add(String.format("  Loops  > %s", loopsLine));
-        }
-        if 
(generator.getProvidedRequirements().contains(TraverserRequirement.SIDE_EFFECTS))
 {
-            final TraversalSideEffects tse = traverser.getSideEffects();
-            final Set<String> keys = tse.keys();
-            lines.add(String.format("  S/E    > %s", 
keys.stream().collect(Collectors.toMap(k -> k, tse::get))));
+        // not sure how you'd have one without the other really
+        if (traverser != null) {
+            lines.add(String.format("Traverser> %s", traverser.toString()));
+
+            if (traversal != null) {
+                final TraverserGenerator generator = 
traversal.getTraverserGenerator();
+                if 
(generator.getProvidedRequirements().contains(TraverserRequirement.BULK)) {
+                    lines.add(String.format("  Bulk   > %s", 
traverser.bulk()));
+                }
+                if 
(generator.getProvidedRequirements().contains(TraverserRequirement.SACK)) {
+                    lines.add(String.format("  Sack   > %s", 
traverser.sack()));
+                }
+                if 
(generator.getProvidedRequirements().contains(TraverserRequirement.PATH)) {
+                    lines.add(String.format("  Path   > %s", 
traverser.path()));
+                }
+                if 
(generator.getProvidedRequirements().contains(TraverserRequirement.SINGLE_LOOP) 
||
+                        
generator.getProvidedRequirements().contains(TraverserRequirement.NESTED_LOOP) 
) {
+                    final Set<String> loopNames = traverser.getLoopNames();
+                    final String loopsLine = loopNames.isEmpty() ?
+                            String.valueOf(traverser.asAdmin().loops()) :
+                            
loopNames.stream().collect(Collectors.toMap(loopName -> loopName, 
traverser::loops)).toString();
+                    lines.add(String.format("  Loops  > %s", loopsLine));
+                }
+                if 
(generator.getProvidedRequirements().contains(TraverserRequirement.SIDE_EFFECTS))
 {
+                    final TraversalSideEffects tse = 
traverser.getSideEffects();
+                    final Set<String> keys = tse.keys();
+                    lines.add(String.format("  S/E    > %s", 
keys.stream().collect(Collectors.toMap(k -> k, tse::get))));
+                }
+            }
         }
 
-        // removes the starting period so that "__.out()" simply presents as 
"out()"
-        lines.add(String.format("Traversal> %s", 
TRANSLATOR.translate(getTraversal()).getScript().substring(1)));
+        if (traversal != null) {
+            // removes the starting period so that "__.out()" simply presents 
as "out()"
+            lines.add(String.format("Traversal> %s", 
TRANSLATOR.translate(traversal).getScript().substring(1)));
 
-        // not sure there is a situation where fail() would be used where it 
was not wrapped in a parent,
-        // but on the odd case that it is it can be handled
-        if (parentStep != EmptyStep.instance()) {
-            lines.add(String.format("Parent   > %s [%s]",
-                    parentStep.getClass().getSimpleName(), 
TRANSLATOR.translate(parentStep.getTraversal()).getScript().substring(1)));
+            // not sure there is a situation where fail() would be used where 
it was not wrapped in a parent,
+            // but on the odd case that it is it can be handled
+            if (parentStep != null && parentStep != EmptyStep.instance()) {
+                lines.add(String.format("Parent   > %s [%s]",
+                        parentStep.getClass().getSimpleName(), 
TRANSLATOR.translate(parentStep.getTraversal()).getScript().substring(1)));
+            }
         }
 
         lines.add(String.format("Metadata > %s", getMetadata()));
diff --git 
a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/FailureTest.java
 
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/FailureTest.java
new file mode 100644
index 0000000000..351a53fce9
--- /dev/null
+++ 
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/FailureTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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.process.traversal;
+
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * Tests for the default methods on the {@link Failure} interface, with 
particular focus on {@link Failure#format()}
+ * gracefully handling {@code Failure} implementations that do not carry 
traversal/traverser context (e.g. those
+ * constructed from a remote response such as {@code FailResponseException}).
+ */
+public class FailureTest {
+
+    /**
+     * Minimal {@link Failure} that returns {@code null} for the traversal and 
traverser context the same way a
+     * {@code Failure} reconstructed from a remote server response would.
+     */
+    private static class RemoteLikeFailure implements Failure {
+        private final String message;
+        private final Map<String, Object> metadata;
+
+        private RemoteLikeFailure(final String message, final Map<String, 
Object> metadata) {
+            this.message = message;
+            this.metadata = metadata;
+        }
+
+        @Override
+        public String getMessage() {
+            return message;
+        }
+
+        @Override
+        public Map<String, Object> getMetadata() {
+            return metadata;
+        }
+
+        @Override
+        public Traverser.Admin getTraverser() {
+            return null;
+        }
+
+        @Override
+        public Traversal.Admin getTraversal() {
+            return null;
+        }
+    }
+
+    @Test
+    public void shouldFormatWithoutTraversalAndTraverserContext() {
+        final Failure failure = new RemoteLikeFailure("make it stop", 
Collections.emptyMap());
+
+        // would throw a NullPointerException prior to the null guards being 
added to format()
+        final String formatted = failure.format();
+
+        assertNotNull(formatted);
+        assertThat(formatted, containsString("fail() Step Triggered"));
+        assertThat(formatted, containsString("Message  > make it stop"));
+        assertThat(formatted, containsString("Metadata > {}"));
+
+        // these sections depend on traversal/traverser context which is 
absent so they should be omitted entirely
+        assertThat(formatted, not(containsString("Traverser>")));
+        assertThat(formatted, not(containsString("Traversal>")));
+        assertThat(formatted, not(containsString("Parent   >")));
+    }
+
+    @Test
+    public void shouldFormatWithNullMessageAndMetadata() {
+        final Failure failure = new RemoteLikeFailure(null, null);
+
+        // null message and metadata should still format without error
+        final String formatted = failure.format();
+
+        assertNotNull(formatted);
+        assertThat(formatted, containsString("Message  > null"));
+        assertThat(formatted, containsString("Metadata > null"));
+    }
+}
diff --git 
a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Handler.java 
b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Handler.java
index 588c54f686..3b1adca514 100644
--- 
a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Handler.java
+++ 
b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Handler.java
@@ -20,6 +20,7 @@ package org.apache.tinkerpop.gremlin.driver;
 
 import io.netty.util.AttributeMap;
 import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.apache.tinkerpop.gremlin.driver.exception.FailResponseException;
 import org.apache.tinkerpop.gremlin.driver.exception.ResponseException;
 import org.apache.tinkerpop.gremlin.util.Tokens;
 import org.apache.tinkerpop.gremlin.util.message.RequestMessage;
@@ -242,8 +243,16 @@ final class Handler {
                             (String) 
attributes.get(Tokens.STATUS_ATTRIBUTE_STACK_TRACE) : null;
                     final List<String> exceptions = 
attributes.containsKey(Tokens.STATUS_ATTRIBUTE_EXCEPTIONS) ?
                             (List<String>) 
attributes.get(Tokens.STATUS_ATTRIBUTE_EXCEPTIONS) : null;
-                    queue.markError(new 
ResponseException(response.getStatus().getCode(), 
response.getStatus().getMessage(),
-                            exceptions, stackTrace, 
cleanStatusAttributes(attributes)));
+
+                    // a SERVER_ERROR_FAIL_STEP indicates that the traversal 
triggered a fail() step on the server.
+                    // throw the more specific FailResponseException which 
implements Failure so that handling can
+                    // be made more consistent with the local fail() behavior.
+                    final ResponseException responseException = statusCode == 
ResponseStatusCode.SERVER_ERROR_FAIL_STEP ?
+                            new 
FailResponseException(response.getStatus().getMessage(), exceptions, stackTrace,
+                                    cleanStatusAttributes(attributes)) :
+                            new 
ResponseException(response.getStatus().getCode(), 
response.getStatus().getMessage(),
+                                    exceptions, stackTrace, 
cleanStatusAttributes(attributes));
+                    queue.markError(responseException);
                 }
             }
 
diff --git 
a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/exception/FailResponseException.java
 
b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/exception/FailResponseException.java
new file mode 100644
index 0000000000..2529349de1
--- /dev/null
+++ 
b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/exception/FailResponseException.java
@@ -0,0 +1,60 @@
+/*
+ * 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.exception;
+
+import org.apache.tinkerpop.gremlin.process.traversal.Failure;
+import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
+import org.apache.tinkerpop.gremlin.process.traversal.Traverser;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
+import org.apache.tinkerpop.gremlin.util.message.ResponseStatusCode;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Provides a {@link Failure} implementation for {@link ResponseException}. 
This exception is thrown instead of
+ * a {@code ResponseException} when the server returns a {@code status.code} of
+ * {@link ResponseStatusCode#SERVER_ERROR_FAIL_STEP} which indicates that a 
step in the traversal failed by way of
+ * {@link GraphTraversal#fail()}. This approach helps make remote exception 
handling for that step more consistent
+ * with the local {@link GraphTraversal#fail()} behavior.
+ */
+public class FailResponseException extends ResponseException implements 
Failure {
+    public FailResponseException(final String serverMessage,
+                                 final List<String> remoteExceptionHierarchy, 
final String remoteStackTrace,
+                                 final Map<String,Object> statusAttributes) {
+        super(ResponseStatusCode.SERVER_ERROR_FAIL_STEP, serverMessage, 
remoteExceptionHierarchy,
+                remoteStackTrace, statusAttributes);
+    }
+
+    @Override
+    public Map<String, Object> getMetadata() {
+        return Collections.emptyMap();
+    }
+
+    @Override
+    public Traverser.Admin getTraverser() {
+        return null;
+    }
+
+    @Override
+    public Traversal.Admin getTraversal() {
+        return null;
+    }
+}
diff --git 
a/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/exception/FailResponseExceptionTest.java
 
b/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/exception/FailResponseExceptionTest.java
new file mode 100644
index 0000000000..53dbea7247
--- /dev/null
+++ 
b/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/exception/FailResponseExceptionTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.exception;
+
+import org.apache.tinkerpop.gremlin.process.traversal.Failure;
+import org.apache.tinkerpop.gremlin.util.message.ResponseStatusCode;
+import org.junit.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public class FailResponseExceptionTest {
+
+    @Test
+    public void shouldBeAResponseExceptionAndFailure() {
+        final FailResponseException ex = new FailResponseException("make it 
stop", null, null, null);
+        assertThat(ex, instanceOf(ResponseException.class));
+        assertThat(ex, instanceOf(Failure.class));
+        assertEquals(ResponseStatusCode.SERVER_ERROR_FAIL_STEP, 
ex.getResponseStatusCode());
+        assertEquals("make it stop", ex.getMessage());
+    }
+
+    @Test
+    public void 
shouldReturnEmptyFailureContextSinceItIsNotTransmittedFromServer() {
+        final FailResponseException ex = new FailResponseException("make it 
stop", null, null, null);
+        assertTrue(ex.getMetadata().isEmpty());
+        assertNull(ex.getTraverser());
+        assertNull(ex.getTraversal());
+    }
+
+    @Test
+    public void shouldFormatWithoutThrowingWhenFailureContextIsAbsent() {
+        final FailResponseException ex = new FailResponseException("make it 
stop", null, null, null);
+
+        // format() must not NPE even though getTraversal()/getTraverser() 
return null for a remotely
+        // reconstructed Failure
+        final String formatted = ex.format();
+
+        assertNotNull(formatted);
+        assertThat(formatted, containsString("fail() Step Triggered"));
+        assertThat(formatted, containsString("Message  > make it stop"));
+        assertThat(formatted, containsString("Metadata > {}"));
+    }
+}
diff --git 
a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerIntegrateTest.java
 
b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerIntegrateTest.java
index df9e680609..5a6d327914 100644
--- 
a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerIntegrateTest.java
+++ 
b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerIntegrateTest.java
@@ -27,6 +27,8 @@ import org.apache.commons.configuration2.BaseConfiguration;
 import org.apache.commons.configuration2.Configuration;
 import org.apache.commons.lang3.RandomStringUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.tinkerpop.gremlin.driver.exception.FailResponseException;
+import org.apache.tinkerpop.gremlin.process.traversal.Failure;
 import org.apache.tinkerpop.gremlin.server.channel.HttpTestChannelizer;
 import org.apache.tinkerpop.gremlin.server.channel.TestChannelizer;
 import org.apache.tinkerpop.gremlin.server.channel.UnifiedChannelizer;
@@ -1376,6 +1378,8 @@ public class GremlinServerIntegrateTest extends 
AbstractGremlinServerIntegration
         } catch (Exception ex) {
             final Throwable t = ex.getCause();
             assertThat(t, instanceOf(ResponseException.class));
+            assertThat(t, instanceOf(FailResponseException.class));
+            assertThat(t, instanceOf(Failure.class));
             assertEquals("make it stop", t.getMessage());
             assertEquals(ResponseStatusCode.SERVER_ERROR_FAIL_STEP, 
((ResponseException) t).getResponseStatusCode());
         }

Reply via email to