You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by va...@apache.org on 2024/04/19 20:47:56 UTC

(tinkerpop) branch valentyn/http-error-handling created (now 42e292cb0b)

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

valentyn pushed a change to branch valentyn/http-error-handling
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git


      at 42e292cb0b errors mapping

This branch includes the following new commits:

     new 42e292cb0b errors mapping

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: errors mapping

Posted by va...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

valentyn pushed a commit to branch valentyn/http-error-handling
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit 42e292cb0b6f9015bb068b83ef02524120b33170
Author: Valentyn Kahamlyk <Va...@improving.com>
AuthorDate: Fri Apr 19 13:47:47 2024 -0700

    errors mapping
---
 .../gremlin/server/ProcessingException.java        |  14 +-
 .../server/handler/HttpGremlinEndpointHandler.java | 162 ++++++-------------
 .../gremlin/server/handler/HttpHandlerUtil.java    |  40 ++---
 .../gremlin/server/util/GremlinError.java          | 173 +++++++++++++++++++++
 .../gremlin/server/HttpDriverIntegrateTest.java    |   2 +-
 .../gremlin/util/message/ResponseStatusCode.java   |  15 ++
 .../ser/AbstractGraphSONMessageSerializerV4.java   |   5 +-
 7 files changed, 263 insertions(+), 148 deletions(-)

diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/ProcessingException.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/ProcessingException.java
index 2174e96e67..a68d9d3eea 100644
--- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/ProcessingException.java
+++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/ProcessingException.java
@@ -18,20 +18,20 @@
  */
 package org.apache.tinkerpop.gremlin.server;
 
-import org.apache.tinkerpop.gremlin.util.message.ResponseMessage;
+import org.apache.tinkerpop.gremlin.server.util.GremlinError;
 
 /**
  * @author Stephen Mallette (http://stephen.genoprime.com)
  */
 public class ProcessingException extends Exception {
-    private final ResponseMessage responseMessage;
+    private final GremlinError error;
 
-    public ProcessingException(final String message, final ResponseMessage responseMessage) {
-        super(message);
-        this.responseMessage = responseMessage;
+    public ProcessingException(final GremlinError error) {
+        super(error.getMessage());
+        this.error = error;
     }
 
-    public ResponseMessage getResponseMessage() {
-        return this.responseMessage;
+    public GremlinError getError() {
+        return this.error;
     }
 }
diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpGremlinEndpointHandler.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpGremlinEndpointHandler.java
index 2196795250..e784826f1d 100644
--- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpGremlinEndpointHandler.java
+++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpGremlinEndpointHandler.java
@@ -49,6 +49,7 @@ import org.apache.tinkerpop.gremlin.server.GremlinServer;
 import org.apache.tinkerpop.gremlin.server.Settings;
 import org.apache.tinkerpop.gremlin.server.auth.AuthenticatedUser;
 import org.apache.tinkerpop.gremlin.server.ProcessingException;
+import org.apache.tinkerpop.gremlin.server.util.GremlinError;
 import org.apache.tinkerpop.gremlin.server.util.MetricManager;
 import org.apache.tinkerpop.gremlin.server.util.TraverserIterator;
 import org.apache.tinkerpop.gremlin.structure.Column;
@@ -81,7 +82,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
-import java.util.UUID;
 import java.util.concurrent.Future;
 import java.util.concurrent.FutureTask;
 import java.util.concurrent.RejectedExecutionException;
@@ -217,21 +217,9 @@ public class HttpGremlinEndpointHandler extends SimpleChannelInboundHandler<Requ
                         iterateTraversal(requestCtx, serializer.getValue1(), translateBytecodeToTraversal(requestCtx));
                         break;
                     case Tokens.OPS_INVALID:
-                        final String msgInvalid =
-                                String.format("Message could not be parsed. Check the format of the request. [%s]", requestMessage);
-                        throw new ProcessingException(msgInvalid,
-                                ResponseMessage.buildV4()
-                                        .code(ResponseStatusCode.REQUEST_ERROR_MALFORMED_REQUEST)
-                                        .statusMessage(msgInvalid)
-                                        .create());
+                        throw new ProcessingException(GremlinError.invalidGremlinType(requestMessage));
                     default:
-                        final String msgDefault =
-                                String.format("Message with gremlin of type [%s] is not recognized.", requestMessage.getGremlinType());
-                        throw new ProcessingException(msgDefault,
-                                ResponseMessage.buildV4()
-                                        .code(ResponseStatusCode.REQUEST_ERROR_MALFORMED_REQUEST)
-                                        .statusMessage(msgDefault)
-                                        .create());
+                        throw new ProcessingException(GremlinError.unknownGremlinType(requestMessage));
                 }
             } catch (Throwable t) {
                 writeError(requestCtx, formErrorResponseMessage(t, requestMessage), serializer.getValue1());
@@ -257,27 +245,16 @@ public class HttpGremlinEndpointHandler extends SimpleChannelInboundHandler<Requ
                 requestCtx.setTimeoutExecutor(requestCtx.getScheduledExecutorService().schedule(() -> {
                     executionFuture.cancel(true);
                     if (!requestCtx.getStartedResponse()) {
-                        final String errorMessage = String.format("A timeout occurred during traversal evaluation of [%s] - consider increasing the limit given to evaluationTimeout", requestMessage);
-                        writeError(requestCtx,
-                                ResponseMessage.buildV4()
-                                        .code(ResponseStatusCode.SERVER_ERROR_TIMEOUT)
-                                        .statusMessage(errorMessage)
-                                        .create(),
-                                serializer.getValue1());
+                        writeError(requestCtx, GremlinError.timeout(requestMessage), serializer.getValue1());
                     }
                 }, seto, TimeUnit.MILLISECONDS));
             }
         } catch (RejectedExecutionException ree) {
-            writeError(requestCtx,
-                    ResponseMessage.buildV4().code(ResponseStatusCode.TOO_MANY_REQUESTS).statusMessage("Rate limiting").create(),
-                    serializer.getValue1());
+            writeError(requestCtx, GremlinError.rateLimiting(), serializer.getValue1());
         }
     }
 
-    private ResponseMessage formErrorResponseMessage(Throwable t, RequestMessageV4 requestMessage) {
-        final ResponseMessage errorResponseMessage;
-        String logMessage = String.format("Exception processing request [%s].", requestMessage);
-
+    private GremlinError formErrorResponseMessage(Throwable t, RequestMessageV4 requestMessage) {
         if (t instanceof UndeclaredThrowableException) t = t.getCause();
 
         // if any exception in the chain is TemporaryException or Failure then we should respond with the
@@ -285,63 +262,43 @@ public class HttpGremlinEndpointHandler extends SimpleChannelInboundHandler<Requ
         final Optional<Throwable> possibleSpecialException = determineIfSpecialException(t);
         if (possibleSpecialException.isPresent()) {
             final Throwable special = possibleSpecialException.get();
-            final ResponseMessage.Builder specialResponseMsg = ResponseMessage.buildV4().
-                    statusMessage(special.getMessage()).
-                    statusAttributeException(special);
             if (special instanceof TemporaryException) {
-                specialResponseMsg.code(ResponseStatusCode.SERVER_ERROR_TEMPORARY);
-            } else if (special instanceof Failure) {
-                final Failure failure = (Failure) special;
-                specialResponseMsg.code(ResponseStatusCode.SERVER_ERROR_FAIL_STEP).
-                        statusAttribute(Tokens.STATUS_ATTRIBUTE_FAIL_STEP_MESSAGE, failure.format());
+                return GremlinError.temporary(special);
             }
-            errorResponseMessage = specialResponseMsg.create();
-        } else if (t instanceof ProcessingException) {
-            errorResponseMessage = ((ProcessingException) t).getResponseMessage();
-        } else {
-            t = ExceptionHelper.getRootCause(t);
-
-            if (t instanceof TooLongFrameException) {
-                return ResponseMessage.buildV4()
-                        .code(ResponseStatusCode.SERVER_ERROR)
-                        .statusMessage(t.getMessage() + " - increase the maxContentLength")
-                        .create();
-            } else if (t instanceof InterruptedException || t instanceof TraversalInterruptedException) {
-                final String errorMessage = String.format("A timeout occurred during traversal evaluation of [%s] - consider increasing the limit given to evaluationTimeout", requestMessage);
-                errorResponseMessage = ResponseMessage.buildV4()
-                        .code(ResponseStatusCode.SERVER_ERROR_TIMEOUT)
-                        .statusMessage(errorMessage)
-                        .statusAttributeException(t)
-                        .create();
-            } else if (t instanceof TimedInterruptTimeoutException) {
-                // occurs when the TimedInterruptCustomizerProvider is in play
-                logMessage = String.format("A timeout occurred within the script during evaluation of [%s] - consider increasing the limit given to TimedInterruptCustomizerProvider", requestMessage);
-                errorResponseMessage = ResponseMessage.buildV4().code(ResponseStatusCode.SERVER_ERROR_TIMEOUT)
-                        .statusMessage("Timeout during script evaluation triggered by TimedInterruptCustomizerProvider")
-                        .statusAttributeException(t).create();
-            } else if (t instanceof TimeoutException) {
-                logMessage = String.format("Script evaluation exceeded the configured threshold for request [%s]", requestMessage);
-                errorResponseMessage = ResponseMessage.buildV4().code(ResponseStatusCode.SERVER_ERROR_TIMEOUT)
-                        .statusMessage(t.getMessage())
-                        .statusAttributeException(t).create();
-            } else if (t instanceof MultipleCompilationErrorsException && t.getMessage().contains("Method too large") &&
-                    ((MultipleCompilationErrorsException) t).getErrorCollector().getErrorCount() == 1) {
-                final String errorMessage = String.format("The Gremlin statement that was submitted exceeds the maximum compilation size allowed by the JVM, please split it into multiple smaller statements - %s", requestMessage.trimMessage(1021));
-                logMessage = errorMessage;
-                errorResponseMessage = ResponseMessage.buildV4().code(ResponseStatusCode.SERVER_ERROR_EVALUATION)
-                        .statusMessage(errorMessage)
-                        .statusAttributeException(t).create();
-            } else {
-                errorResponseMessage = ResponseMessage.buildV4()
-                        .code(ResponseStatusCode.SERVER_ERROR_EVALUATION)
-                        .statusMessage((t.getMessage() == null) ? t.toString() : t.getMessage())
-                        .statusAttributeException(t)
-                        .create();
+            if (special instanceof Failure) {
+                return GremlinError.failStep((Failure) special);
             }
+            return GremlinError.general(special);
+        }
+        if (t instanceof ProcessingException) {
+            return ((ProcessingException) t).getError();
+        }
+        t = ExceptionHelper.getRootCause(t);
+
+        if (t instanceof TooLongFrameException) {
+            return GremlinError.longFrame(t);
+        }
+        if (t instanceof InterruptedException || t instanceof TraversalInterruptedException) {
+            return GremlinError.timeout(requestMessage);
+        }
+        if (t instanceof TimedInterruptTimeoutException) {
+            // occurs when the TimedInterruptCustomizerProvider is in play
+            logger.warn(String.format("A timeout occurred within the script during evaluation of [%s] - consider increasing the limit given to TimedInterruptCustomizerProvider", requestMessage));
+            return GremlinError.timedInterruptTimeout();
+        }
+        if (t instanceof TimeoutException) {
+            logger.warn(String.format("Script evaluation exceeded the configured threshold for request [%s]", requestMessage));
+            return GremlinError.timeout(requestMessage);
+        }
+        if (t instanceof MultipleCompilationErrorsException && t.getMessage().contains("Method too large") &&
+                ((MultipleCompilationErrorsException) t).getErrorCollector().getErrorCount() == 1) {
+            final GremlinError error = GremlinError.longRequest(requestMessage);
+            logger.warn(error.getMessage());
+            return error;
         }
 
-        logger.warn(logMessage, t);
-        return errorResponseMessage;
+        logger.warn(String.format("Exception processing request [%s].", requestMessage));
+        return GremlinError.general(t);
     }
 
     private void iterateScriptEvalResult(final Context context, MessageTextSerializerV4<?> serializer, final RequestMessageV4 message)
@@ -349,21 +306,17 @@ public class HttpGremlinEndpointHandler extends SimpleChannelInboundHandler<Requ
         if (message.optionalField(Tokens.ARGS_BINDINGS).isPresent()) {
             final Map bindings = (Map) message.getFields().get(Tokens.ARGS_BINDINGS);
             if (IteratorUtils.anyMatch(bindings.keySet().iterator(), k -> null == k || !(k instanceof String))) {
-                final String msg = String.format("The [%s] message is using one or more invalid binding keys - they must be of type String and cannot be null", Tokens.OPS_EVAL);
-                throw new ProcessingException(msg, ResponseMessage.buildV4().code(ResponseStatusCode.REQUEST_ERROR_INVALID_REQUEST_ARGUMENTS).statusMessage(msg).create());
+                throw new ProcessingException(GremlinError.binding());
             }
 
             final Set<String> badBindings = IteratorUtils.set(IteratorUtils.<String>filter(bindings.keySet().iterator(), INVALID_BINDINGS_KEYS::contains));
             if (!badBindings.isEmpty()) {
-                final String msg = String.format("The [%s] message supplies one or more invalid parameters key of [%s] - these are reserved names.", Tokens.OPS_EVAL, badBindings);
-                throw new ProcessingException(msg, ResponseMessage.buildV4().code(ResponseStatusCode.REQUEST_ERROR_INVALID_REQUEST_ARGUMENTS).statusMessage(msg).create());
+                throw new ProcessingException(GremlinError.binding(badBindings));
             }
 
             // ignore control bindings that get passed in with the "#jsr223" prefix - those aren't used in compilation
             if (IteratorUtils.count(IteratorUtils.filter(bindings.keySet().iterator(), k -> !k.toString().startsWith("#jsr223"))) > settings.maxParameters) {
-                final String msg = String.format("The [%s] message contains %s bindings which is more than is allowed by the server %s configuration",
-                        Tokens.OPS_EVAL, bindings.size(), settings.maxParameters);
-                throw new ProcessingException(msg, ResponseMessage.buildV4().code(ResponseStatusCode.REQUEST_ERROR_INVALID_REQUEST_ARGUMENTS).statusMessage(msg).create());
+                throw new ProcessingException(GremlinError.binding(bindings.size(), settings.maxParameters));
             }
         }
 
@@ -380,21 +333,17 @@ public class HttpGremlinEndpointHandler extends SimpleChannelInboundHandler<Requ
     private static Traversal.Admin<?,?> translateBytecodeToTraversal(Context ctx) throws ProcessingException {
         final RequestMessageV4 requestMsg = ctx.getRequestMessage();
         if (!(requestMsg.getGremlin() instanceof Bytecode)) {
-            final String msg = String.format("A [%s] message requires a gremlin argument that is of type %s.",
-                    Tokens.OPS_BYTECODE, Tokens.ARGS_GREMLIN, Bytecode.class.getSimpleName());
-            throw new ProcessingException(msg, ResponseMessage.buildV4().code(ResponseStatusCode.REQUEST_ERROR_INVALID_REQUEST_ARGUMENTS).statusMessage(msg).create());
+            throw new ProcessingException(GremlinError.gremlinType());
         }
 
         final Optional<String> alias = requestMsg.optionalField(Tokens.ARGS_G);
         if (!alias.isPresent()) {
-            final String msg = String.format("A [%s] message requires a [%s] argument.", Tokens.OPS_BYTECODE, Tokens.ARGS_G);
-            throw new ProcessingException(msg, ResponseMessage.buildV4().code(ResponseStatusCode.REQUEST_ERROR_INVALID_REQUEST_ARGUMENTS).statusMessage(msg).create());
+            throw new ProcessingException(GremlinError.traversalSource());
         }
 
         final String traversalSourceName = alias.get();
         if (!ctx.getGraphManager().getTraversalSourceNames().contains(traversalSourceName)) {
-            final String msg = String.format("The traversal source [%s] for alias [%s] is not configured on the server.", traversalSourceName, Tokens.VAL_TRAVERSAL_SOURCE_ALIAS);
-            throw new ProcessingException(msg, ResponseMessage.buildV4().code(ResponseStatusCode.REQUEST_ERROR_INVALID_REQUEST_ARGUMENTS).statusMessage(msg).create());
+            throw new ProcessingException(GremlinError.traversalSource(traversalSourceName));
         }
 
         final TraversalSource g = ctx.getGraphManager().getTraversalSource(traversalSourceName);
@@ -407,16 +356,10 @@ public class HttpGremlinEndpointHandler extends SimpleChannelInboundHandler<Requ
                 return ctx.getGremlinExecutor().eval(bytecode, EMPTY_BINDINGS, lambdaLanguage.get(), traversalSourceName);
         } catch (ScriptException ex) {
             logger.error("Traversal contains a lambda that cannot be compiled", ex);
-            throw new ProcessingException("Traversal contains a lambda that cannot be compiled",
-                    ResponseMessage.buildV4().code(ResponseStatusCode.SERVER_ERROR_EVALUATION)
-                            .statusMessage(ex.getMessage())
-                            .statusAttributeException(ex).create());
+            throw new ProcessingException(GremlinError.lambdaNotSupported(ex));
         } catch (Exception ex) {
             logger.error("Could not deserialize the Traversal instance", ex);
-            throw new ProcessingException("Could not deserialize the Traversal instance",
-                    ResponseMessage.buildV4().code(ResponseStatusCode.SERVER_ERROR_SERIALIZATION)
-                            .statusMessage(ex.getMessage())
-                            .statusAttributeException(ex).create());
+            throw new ProcessingException(GremlinError.deserializeTraversal(ex));
         }
     }
 
@@ -456,10 +399,7 @@ public class HttpGremlinEndpointHandler extends SimpleChannelInboundHandler<Requ
             // this validation is important to calls to GraphManager.commit() and rollback() as they both
             // expect that the aliases supplied are valid
             if (!found) {
-                final String error = String.format("Could not alias [%s] to [%s] as [%s] not in the Graph or TraversalSource global bindings",
-                        Tokens.ARGS_G, aliased, aliased);
-                throw new ProcessingException(error, ResponseMessage.buildV4()
-                        .code(ResponseStatusCode.REQUEST_ERROR_INVALID_REQUEST_ARGUMENTS).statusMessage(error).create());
+                throw new ProcessingException(GremlinError.binding(aliased));
             }
         }
 
@@ -658,13 +598,7 @@ public class HttpGremlinEndpointHandler extends SimpleChannelInboundHandler<Requ
 
         } catch (Exception ex) {
             logger.warn("The result [{}] in the request {} could not be serialized and returned.", aggregate, msg.getRequestId(), ex);
-            final String errorMessage = String.format("Error during serialization: %s", ExceptionHelper.getMessageFromExceptionOrCause(ex));
-            writeError(ctx,
-                    ResponseMessage.build(msg.getRequestId())
-                            .statusMessage(errorMessage)
-                            .statusAttributeException(ex)
-                            .code(ResponseStatusCode.SERVER_ERROR_SERIALIZATION).create(),
-                    serializer);
+            writeError(ctx, GremlinError.serialization(ex), serializer);
             throw ex;
         }
     }
diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpHandlerUtil.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpHandlerUtil.java
index 61f16280cc..bfdf18fe24 100644
--- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpHandlerUtil.java
+++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpHandlerUtil.java
@@ -21,56 +21,36 @@ package org.apache.tinkerpop.gremlin.server.handler;
 import com.codahale.metrics.Meter;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
-import io.netty.channel.ChannelFuture;
-import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.handler.codec.http.DefaultFullHttpResponse;
 import io.netty.handler.codec.http.DefaultHttpContent;
 import io.netty.handler.codec.http.DefaultLastHttpContent;
-import io.netty.handler.codec.http.FullHttpRequest;
 import io.netty.handler.codec.http.FullHttpResponse;
-import io.netty.handler.codec.http.HttpHeaderNames;
 import io.netty.handler.codec.http.HttpResponseStatus;
 import io.netty.handler.codec.http.HttpUtil;
 import io.netty.util.CharsetUtil;
-import org.apache.commons.lang3.exception.ExceptionUtils;
 import org.apache.tinkerpop.gremlin.server.Context;
 import org.apache.tinkerpop.gremlin.server.GremlinServer;
-import org.apache.tinkerpop.gremlin.server.Settings;
+import org.apache.tinkerpop.gremlin.server.util.GremlinError;
 import org.apache.tinkerpop.gremlin.server.util.MetricManager;
 import org.apache.tinkerpop.gremlin.util.MessageSerializer;
-import org.apache.tinkerpop.gremlin.util.Tokens;
-import org.apache.tinkerpop.gremlin.util.message.RequestMessage;
-import org.apache.tinkerpop.gremlin.util.message.RequestMessageV4;
 import org.apache.tinkerpop.gremlin.util.message.ResponseMessage;
 import org.apache.tinkerpop.gremlin.util.message.ResponseStatusCode;
 import org.apache.tinkerpop.gremlin.util.ser.MessageTextSerializerV4;
 import org.apache.tinkerpop.gremlin.util.ser.SerTokens;
 import org.apache.tinkerpop.gremlin.util.ser.SerializationException;
-import org.apache.tinkerpop.shaded.jackson.databind.JsonNode;
 import org.apache.tinkerpop.shaded.jackson.databind.ObjectMapper;
-import org.apache.tinkerpop.shaded.jackson.databind.node.ArrayNode;
 import org.apache.tinkerpop.shaded.jackson.databind.node.ObjectNode;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Optional;
-import java.util.UUID;
 
 import static com.codahale.metrics.MetricRegistry.name;
 import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
-import static io.netty.handler.codec.http.HttpMethod.POST;
 import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
-import static io.netty.handler.codec.http.LastHttpContent.EMPTY_LAST_CONTENT;
 
 /**
  * Provides methods shared by the HTTP handlers.
@@ -90,9 +70,9 @@ public class HttpHandlerUtil {
      * Helper method to send errors back as JSON. Only to be used when the RequestMessage couldn't be parsed, because
      * a proper serialized ResponseMessage should be sent in that case.
      *
-     * @param ctx       The netty channel context.
-     * @param status    The HTTP error status code.
-     * @param message   The error message to contain the body.
+     * @param ctx     The netty channel context.
+     * @param status  The HTTP error status code.
+     * @param message The error message to contain the body.
      */
     public static void sendError(final ChannelHandlerContext ctx, final HttpResponseStatus status, final String message) {
         logger.warn(String.format("Invalid request - responding with %s and %s", status, message));
@@ -108,7 +88,7 @@ public class HttpHandlerUtil {
         ctx.writeAndFlush(response);
     }
 
-    static void writeError(final Context context, ResponseMessage responseMessage, final MessageSerializer<?> serializer) {
+    static void writeError(final Context context, final ResponseMessage responseMessage, final MessageSerializer<?> serializer) {
         try {
             final ChannelHandlerContext ctx = context.getChannelHandlerContext();
             final ByteBuf ByteBuf = context.getRequestState() == HttpGremlinEndpointHandler.RequestState.STREAMING
@@ -124,6 +104,16 @@ public class HttpHandlerUtil {
         }
     }
 
+    static void writeError(final Context context, final GremlinError error, final MessageSerializer<?> serializer) {
+        final ResponseMessage responseMessage = ResponseMessage.buildV4()
+                .code(error.getCode())
+                .statusMessage(error.getMessage())
+                .exception(error.getException())
+                .create();
+
+        writeError(context, responseMessage, serializer);
+    }
+
     static void sendTrailingHeaders(final ChannelHandlerContext ctx, final ResponseStatusCode statusCode, final String message) {
         final DefaultLastHttpContent defaultLastHttpContent = new DefaultLastHttpContent();
         defaultLastHttpContent.trailingHeaders().add(SerTokens.TOKEN_CODE, statusCode.getValue());
diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/GremlinError.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/GremlinError.java
new file mode 100644
index 0000000000..1e1185e3fd
--- /dev/null
+++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/GremlinError.java
@@ -0,0 +1,173 @@
+/*
+ * 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.util;
+
+import org.apache.tinkerpop.gremlin.process.traversal.Failure;
+import org.apache.tinkerpop.gremlin.util.ExceptionHelper;
+import org.apache.tinkerpop.gremlin.util.Tokens;
+import org.apache.tinkerpop.gremlin.util.message.RequestMessageV4;
+import org.apache.tinkerpop.gremlin.util.message.ResponseStatusCode;
+
+import java.util.Set;
+
+public class GremlinError {
+    private final ResponseStatusCode code;
+    private final String message;
+    private final String exception;
+
+    private GremlinError(ResponseStatusCode code, String message, String exception) {
+
+        this.code = code;
+        this.message = message;
+        this.exception = exception;
+    }
+
+    public ResponseStatusCode getCode() {
+        return code;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public String getException() {
+        return exception;
+    }
+
+    // ------------ request validation errors
+
+    // script type errors
+    public static GremlinError invalidGremlinType(final RequestMessageV4 requestMessage ) {
+        final String message = String.format("Message could not be parsed. Check the format of the request. [%s]",
+                requestMessage);
+        return new GremlinError(ResponseStatusCode.BAD_REQUEST, message, "InvalidRequestException");
+    }
+
+    public static GremlinError unknownGremlinType(final RequestMessageV4 requestMessage ) {
+        final String message = String.format("Message with gremlin of type [%s] is not recognized.",
+                requestMessage.getGremlinType());
+        return new GremlinError(ResponseStatusCode.BAD_REQUEST, message, "InvalidRequestException");
+    }
+
+    // script errors
+    public static GremlinError binding() {
+        final String message = String.format("The [%s] message is using one or more invalid binding keys - they must be of type String and cannot be null",
+                Tokens.OPS_EVAL);
+        return new GremlinError(ResponseStatusCode.BAD_REQUEST, message, "InvalidRequestException");
+    }
+
+    public static GremlinError binding(final Set<String> badBindings) {
+        final String message = String.format("The [%s] message supplies one or more invalid parameters key of [%s] - these are reserved names.",
+                Tokens.OPS_EVAL, badBindings);
+        return new GremlinError(ResponseStatusCode.BAD_REQUEST, message, "InvalidRequestException");
+    }
+
+    public static GremlinError binding(final int bindingsCount, final int allowedSize) {
+        final String message = String.format("The [%s] message contains %s bindings which is more than is allowed by the server %s configuration",
+                Tokens.OPS_EVAL, bindingsCount, allowedSize);
+        return new GremlinError(ResponseStatusCode.BAD_REQUEST, message, "InvalidRequestException");
+    }
+
+    public static GremlinError binding(final String aliased) {
+        final String message = String.format("Could not alias [%s] to [%s] as [%s] not in the Graph or TraversalSource global bindings",
+                Tokens.ARGS_G, aliased, aliased);
+        return new GremlinError(ResponseStatusCode.BAD_REQUEST, message, "InvalidRequestException");
+    }
+
+    // bytecode errors
+
+    public static GremlinError gremlinType() {
+        final String message = String.format("A [%s] message requires a gremlin argument that is of type %s.",
+                Tokens.OPS_BYTECODE, Tokens.ARGS_GREMLIN);
+        return new GremlinError(ResponseStatusCode.BAD_REQUEST, message, "InvalidRequestException");
+    }
+
+    public static GremlinError traversalSource() {
+        final String message = String.format("A [%s] message requires a [%s] argument.", Tokens.OPS_BYTECODE, Tokens.ARGS_G);
+        return new GremlinError(ResponseStatusCode.BAD_REQUEST, message, "InvalidRequestException");
+    }
+
+    public static GremlinError traversalSource(final String traversalSourceName ) {
+        final String message = String.format("The traversal source [%s] for alias [%s] is not configured on the server.",
+                traversalSourceName, Tokens.VAL_TRAVERSAL_SOURCE_ALIAS);
+        return new GremlinError(ResponseStatusCode.BAD_REQUEST, message, "InvalidRequestException");
+    }
+
+    public static GremlinError lambdaNotSupported(final Throwable t) {
+        return new GremlinError(ResponseStatusCode.BAD_REQUEST, t.getMessage(), "InvalidRequestException");
+    }
+
+    public static GremlinError deserializeTraversal(final Throwable t) {
+        return new GremlinError(ResponseStatusCode.BAD_REQUEST, t.getMessage(), "InvalidRequestException");
+    }
+
+    // execution errors
+    public static GremlinError timeout(final RequestMessageV4 requestMessage ) {
+        final String message = String.format("A timeout occurred during traversal evaluation of [%s] - consider increasing the limit given to evaluationTimeout",
+                requestMessage);
+        return new GremlinError(ResponseStatusCode.SERVER_ERROR, message, "ServerTimeoutExceededException");
+    }
+
+    public static GremlinError timedInterruptTimeout() {
+        return new GremlinError(ResponseStatusCode.SERVER_ERROR,
+                "Timeout during script evaluation triggered by TimedInterruptCustomizerProvider",
+                "ServerTimeoutExceededException");
+    }
+
+    public static GremlinError rateLimiting() {
+        return new GremlinError(ResponseStatusCode.TOO_MANY_REQUESTS,
+                "Too many requests have been sent in a given amount of time.", "TooManyRequestsException");
+    }
+
+    public static GremlinError serialization(Exception ex) {
+        final String message = String.format("Error during serialization: %s", ExceptionHelper.getMessageFromExceptionOrCause(ex));
+        return new GremlinError(ResponseStatusCode.SERVER_ERROR, message, "ServerSerializationException");
+    }
+
+    public static GremlinError wrongSerializer(Exception ex) {
+        final String message = String.format("Error during serialization: %s", ExceptionHelper.getMessageFromExceptionOrCause(ex));
+        return new GremlinError(ResponseStatusCode.SERVER_ERROR, message, "ServerSerializationException");
+    }
+
+    public static GremlinError longFrame(Throwable t) {
+        final String message = t.getMessage() + " - increase the maxContentLength";
+        // todo: ResponseEntityTooLargeException? !!!
+        return new GremlinError(ResponseStatusCode.PAYLOAD_TOO_LARGE, message, "RequestEntityTooLargeException");
+    }
+
+    public static GremlinError longRequest(final RequestMessageV4 requestMessage ) {
+        final String message = String.format("The Gremlin statement that was submitted exceeds the maximum compilation size allowed by the JVM, please split it into multiple smaller statements - %s", requestMessage.trimMessage(1021));
+        return new GremlinError(ResponseStatusCode.PAYLOAD_TOO_LARGE, message, "RequestEntityTooLargeException");
+    }
+
+    public static GremlinError temporary(final Throwable t) {
+        return new GremlinError(ResponseStatusCode.SERVER_ERROR, t.getMessage(), "ServerEvaluationException");
+    }
+
+    public static GremlinError failStep(final Failure failure) {
+        // todo: double check message
+        return new GremlinError(ResponseStatusCode.SERVER_ERROR,
+                failure.getMessage() + ". " + failure.format(), "ServerFailStepException");
+    }
+
+    public static GremlinError general(final Throwable t) {
+        final String message = (t.getMessage() == null) ? t.toString() : t.getMessage();
+        return new GremlinError(ResponseStatusCode.SERVER_ERROR, message, "ServerErrorException");
+    }
+}
diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/HttpDriverIntegrateTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/HttpDriverIntegrateTest.java
index 40bf9dd811..ff61b50d08 100644
--- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/HttpDriverIntegrateTest.java
+++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/HttpDriverIntegrateTest.java
@@ -486,7 +486,7 @@ public class HttpDriverIntegrateTest extends AbstractGremlinServerIntegrationTes
             } catch (Exception ex) {
                 final Throwable inner = ExceptionHelper.getRootCause(ex);
                 assertTrue(inner instanceof ResponseException);
-                assertEquals(ResponseStatusCode.SERVER_ERROR_SERIALIZATION, ((ResponseException) inner).getResponseStatusCode());
+                assertEquals(ResponseStatusCode.SERVER_ERROR, ((ResponseException) inner).getResponseStatusCode());
             }
 
             // should not die completely just because we had a bad serialization error.  that kind of stuff happens
diff --git a/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/message/ResponseStatusCode.java b/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/message/ResponseStatusCode.java
index 98c727e7c0..45a2eed281 100644
--- a/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/message/ResponseStatusCode.java
+++ b/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/message/ResponseStatusCode.java
@@ -59,6 +59,14 @@ public enum ResponseStatusCode {
      */
     PARTIAL_CONTENT(206),
 
+    /**
+     * The server cannot or will not process the request due to an apparent client error (e.g., malformed request
+     * syntax, size too large, invalid request message framing, or deceptive request routing).
+     *
+     * @since 4.0.0
+     */
+    BAD_REQUEST(400),
+
     /**
      * Code 401: The server could not authenticate the request or the client requested a resource it did not have
      * access to.
@@ -82,6 +90,13 @@ public enum ResponseStatusCode {
      */
     AUTHENTICATE(407),
 
+    /**
+     * The request is larger than the server is willing or able to process.
+     *
+     * @since 4.0.0
+     */
+    PAYLOAD_TOO_LARGE(413),
+
     /**
      * Code 429: Indicates that too many requests have been sent in a given amount of time.
      *
diff --git a/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/ser/AbstractGraphSONMessageSerializerV4.java b/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/ser/AbstractGraphSONMessageSerializerV4.java
index 489bf0ac4f..0b3a5b69ba 100644
--- a/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/ser/AbstractGraphSONMessageSerializerV4.java
+++ b/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/ser/AbstractGraphSONMessageSerializerV4.java
@@ -366,7 +366,10 @@ public abstract class AbstractGraphSONMessageSerializerV4 extends AbstractGraphS
             GraphSONUtil.writeStartObject(responseMessage, jsonGenerator, typeSerializer);
             jsonGenerator.writeStringField(SerTokens.TOKEN_MESSAGE, responseMessage.getStatus().getMessage());
             jsonGenerator.writeNumberField(SerTokens.TOKEN_CODE, responseMessage.getStatus().getCode().getValue());
-            jsonGenerator.writeStringField(SerTokens.TOKEN_EXCEPTION, responseMessage.getStatus().getException());
+            if (responseMessage.getStatus().getCode() != ResponseStatusCode.SUCCESS &&
+                    responseMessage.getStatus().getException() != null) {
+                jsonGenerator.writeStringField(SerTokens.TOKEN_EXCEPTION, responseMessage.getStatus().getException());
+            }
             GraphSONUtil.writeEndObject(responseMessage, jsonGenerator, typeSerializer);
 
             GraphSONUtil.writeEndObject(responseMessage, jsonGenerator, typeSerializer);