Repository: eclipse-vertx/vert.x

Problem statement:
Inconsistent error handling with JsonParser
### Version

4.2.6

### Context

I use the JsonParser class to handle a stream of JSON comming from a client request (code follows). When an error is present in the JSON (wrong integer value, additional comma etc ...) everything works as expected, exception handler is called and the code flow continue, the event handler is called with a JSON without the unparseable token (which is fine in my case but maybe weird in others, is it expected/configurable ?). When there is more than one error in the JSON though, the exception handler is called only twice and the json event handler isn't called nor the end handler ...

### Do you have a reproducer?

```java
router.post("/test")
        .handler(ctx -> JsonParser.newParser(ctx.request()).objectValueMode()
                .handler(event -> System.out.println("event received"))
                .exceptionHandler(ex -> System.out.println(ex.getMessage()))
                .endHandler(ignored -> { System.out.println("done"); ctx.response().end("done\n"); }));
```

### Steps to reproduce

1. Run the reproducer in a verticle
2. Call the road with a payload like `{"test":"value"}{"test1":"value1"}{"test2":"value2"}`, observe an expected behaviour
3. Call the road with a payload like `{"test":"value"},{"test1":"value1"}{"test2":"value2"}`, observe an expected behaviour
4. Call the road with a payload like `{"test":"value"},{"test1":"value1"},{"test2":"value2"}`, observe an unexpected behaviour

### Extra

jdk11


PS:

I found a work around in the meantime. I could open a PR, but really, I'm not sure it's a good solution, here it [is](https://github.com/MYDIH/vert.x/commit/efcb5d3805ee7f41da1a09caaf8c721f13e9990a)

It introduces some difference between the case where an exception handler is set and the case where it's not, and it's not tested in case of Json input exceeding a single buffer ... 


Hints:

Patch:
diff --git a/src/main/java/io/vertx/core/parsetools/impl/JsonParserImpl.java b/src/main/java/io/vertx/core/parsetools/impl/JsonParserImpl.java
--- a/src/main/java/io/vertx/core/parsetools/impl/JsonParserImpl.java
+++ b/src/main/java/io/vertx/core/parsetools/impl/JsonParserImpl.java
@@ -12,6 +12,7 @@
 package io.vertx.core.parsetools.impl;
 
 import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonLocation;
 import com.fasterxml.jackson.core.JsonToken;
 import com.fasterxml.jackson.core.ObjectCodec;
 import com.fasterxml.jackson.core.base.ParserBase;
@@ -31,10 +32,7 @@
 import io.vertx.core.streams.ReadStream;
 
 import java.io.IOException;
-import java.util.ArrayDeque;
-import java.util.Deque;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 
 /**
  * @author <a href="mailto:julien@julienviet.com">Julien Viet</a>
@@ -54,6 +52,7 @@ public class JsonParserImpl implements JsonParser {
   private final ReadStream<Buffer> stream;
   private boolean emitting;
   private final Deque<JsonEventImpl> pending = new ArrayDeque<>();
+  private List<IOException> collectedExceptions;
 
   public JsonParserImpl(ReadStream<Buffer> stream) {
     this.stream = stream;
@@ -143,21 +142,24 @@ private void handleEvent(JsonEventImpl event) {
     }
   }
 
+  private void handle(IOException ioe) {
+    if (collectedExceptions == null) {
+      collectedExceptions = new ArrayList<>();
+    }
+    collectedExceptions.add(ioe);
+  }
+
   @Override
   public void handle(Buffer data) {
     byte[] bytes = data.getBytes();
     try {
       parser.feedInput(bytes, 0, bytes.length);
-      checkTokens();
     } catch (IOException e) {
-      if (exceptionHandler != null) {
-        exceptionHandler.handle(e);
-        return;
-      } else {
-        throw new DecodeException(e.getMessage(), e);
-      }
+      handle(e);
     }
+    checkTokens();
     checkPending();
+    checkExceptions();
   }
 
   @Override
@@ -167,25 +169,33 @@ public void end() {
     }
     ended = true;
     parser.endOfInput();
-    try {
-      checkTokens();
-    } catch (IOException e) {
-      if (exceptionHandler != null) {
-        exceptionHandler.handle(e);
-        return;
-      } else {
-        throw new DecodeException(e.getMessage(), e);
-      }
-    }
+    checkTokens();
     checkPending();
+    checkExceptions();
   }
 
-  private void checkTokens() throws IOException {
+  private void checkTokens() {
+    JsonLocation prevLocation = null;
     while (true) {
-      JsonToken token = parser.nextToken();
+      JsonToken token;
+      try {
+        token = parser.nextToken();
+      } catch (IOException e) {
+        JsonLocation location = parser.currentLocation();
+        if (prevLocation != null) {
+          if (location.equals(prevLocation)) {
+            // If we haven't done any progress, give up
+            return;
+          }
+        }
+        prevLocation = location;
+        handle(e);
+        continue;
+      }
       if (token == null || token == JsonToken.NOT_AVAILABLE) {
         break;
       }
+      prevLocation = null;
       String field = currentField;
       currentField = null;
       JsonEventImpl event;
@@ -199,11 +209,20 @@ private void checkTokens() throws IOException {
           break;
         }
         case FIELD_NAME: {
-          currentField = parser.getCurrentName();
+          try {
+            currentField = parser.getCurrentName();
+          } catch (IOException e) {
+            handle(e);
+          }
           continue;
         }
         case VALUE_STRING: {
-          event = new JsonEventImpl(token, JsonEventType.VALUE, field, parser.getText());
+          try {
+            event = new JsonEventImpl(token, JsonEventType.VALUE, field, parser.getText());
+          } catch (IOException e) {
+            handle(e);
+            continue;
+          }
           break;
         }
         case VALUE_TRUE: {
@@ -219,11 +238,21 @@ private void checkTokens() throws IOException {
           break;
         }
         case VALUE_NUMBER_INT: {
-          event = new JsonEventImpl(token, JsonEventType.VALUE, field, parser.getLongValue());
+          try {
+            event = new JsonEventImpl(token, JsonEventType.VALUE, field, parser.getLongValue());
+          } catch (IOException e) {
+            handle(e);
+            continue;
+          }
           break;
         }
         case VALUE_NUMBER_FLOAT: {
-          event = new JsonEventImpl(token, JsonEventType.VALUE, field, parser.getDoubleValue());
+          try {
+            event = new JsonEventImpl(token, JsonEventType.VALUE, field, parser.getDoubleValue());
+          } catch (IOException e) {
+            handle(e);
+            continue;
+          }
           break;
         }
         case END_OBJECT: {
@@ -288,6 +317,21 @@ private void checkPending() {
     }
   }
 
+  private void checkExceptions() {
+    List<IOException> exceptions = collectedExceptions;
+    collectedExceptions = null;
+    if (exceptions != null && exceptions.size() > 0) {
+      if (exceptionHandler != null) {
+        for (IOException ioe : exceptions) {
+          exceptionHandler.handle(ioe);
+        }
+      } else {
+        IOException ioe = exceptions.get(0);
+        throw new DecodeException(ioe.getMessage(), ioe);
+      }
+    }
+  }
+
   @Override
   public JsonParser objectEventMode() {
     objectValueMode = false;


Test patch:
diff --git a/src/test/java/io/vertx/core/parsetools/JsonParserTest.java b/src/test/java/io/vertx/core/parsetools/JsonParserTest.java
--- a/src/test/java/io/vertx/core/parsetools/JsonParserTest.java
+++ b/src/test/java/io/vertx/core/parsetools/JsonParserTest.java
@@ -113,6 +113,35 @@ public void parseUnfinished() {
     assertEquals(1, errors.size());
   }
 
+  @Test
+  public void testParseWithErrors() {
+    Buffer data = Buffer.buffer("{\"foo\":\"foo_value\"},{\"bar\":\"bar_value\"},{\"juu\":\"juu_value\"}");
+    JsonParser parser = JsonParser.newParser();
+    List<JsonObject> objects = new ArrayList<>();
+    List<Throwable> errors = new ArrayList<>();
+    AtomicInteger endCount = new AtomicInteger();
+    parser.objectValueMode()
+      .handler(event -> objects.add(event.objectValue()))
+      .exceptionHandler(errors::add)
+      .endHandler(v -> endCount.incrementAndGet());
+    parser.write(data);
+    assertEquals(3, objects.size());
+    List<JsonObject> expected = Arrays.asList(
+      new JsonObject().put("foo", "foo_value"),
+      new JsonObject().put("bar", "bar_value"),
+      new JsonObject().put("juu", "juu_value")
+    );
+    assertEquals(expected, objects);
+    assertEquals(2, errors.size());
+    assertEquals(0, endCount.get());
+    objects.clear();
+    errors.clear();
+    parser.end();
+    assertEquals(Collections.emptyList(), objects);
+    assertEquals(Collections.emptyList(), errors);
+    assertEquals(1, endCount.get());
+  }
+
   @Test
   public void parseNumberFormatException() {
     Buffer data = Buffer.buffer(Long.MAX_VALUE + "0");