You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by sp...@apache.org on 2026/06/15 20:25:54 UTC
(tinkerpop) branch TINKERPOP-3238 created (now 88b222949c)
This is an automated email from the ASF dual-hosted git repository.
spmallette pushed a change to branch TINKERPOP-3238
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
at 88b222949c TINKERPOP-3238 Throw FailResponseException for fail() step over remote
This branch includes the following new commits:
new 88b222949c TINKERPOP-3238 Throw FailResponseException for fail() step over remote
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(tinkerpop) 01/01: TINKERPOP-3238 Throw FailResponseException for fail() step over remote
Posted by sp...@apache.org.
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 <st...@amazon.com>
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());
}