Repository: vert-x3/vertx-web
Issue: RoutingContext.isFresh() returns false for IF-NONE-MATCH single value Header

Hi,

while trying to use e-tag header for HTTP caching - I noticed the following behavior.

### Version

4.0.0

### Context

Given a simple HTTP server sample using `ctx.isFresh()` method:

```java
package io.vertx.reproducer;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.Promise;
import io.vertx.ext.web.Router;

public class HttpServerVerticle extends AbstractVerticle {

    @Override
    public void start(Promise<Void> startPromise) throws Exception {
        final Router router = Router.router(vertx);

        router.route("/check/e-tag").handler(ctx -> {
            ctx.etag("1234");

            if (ctx.isFresh()) {
                ctx.response().setStatusCode(304).end("Not Modified");
            } else {
                ctx.response().setStatusCode(200).end("OK");
            }
        });

        vertx.createHttpServer()
             .requestHandler(router)
             .listen(8080)
             .onFailure(startPromise::fail)
             .onSuccess(server -> startPromise.complete());
    }

}
```

When sending IF-NONE-MATCH header with value that is matching the ETag:

```shell
> curl -v 'http://localhost:8080/check/e-tag' -H 'IF-NONE-MATCH: "1234"'
*   Trying ::1:8080...
* Connected to localhost (::1) port 8080 (#0)
> GET /check/e-tag HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.74.0
> Accept: */*
> IF-NONE-MATCH: "1234"
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< etag: "1234"
< content-length: 2
< 
* Connection #0 to host localhost left intact
OK% 
```

I expect `ctx.isFresh()` to return `true` and my server to respond with Not Modified.

Inspecting the [code](https://github.com/vert-x3/vertx-web/blob/master/vertx-web/src/main/java/io/vertx/ext/web/impl/Utils.java#L171) it seems to me, that the only way an e-tag is not marked as stale is to have a `,` character in it. That might work when you send multiple values, but am I supposed to send a trailing comma for single value?

```shell
> curl -v 'http://localhost:8080/check/e-tag' -H 'IF-NONE-MATCH: "1234",'
*   Trying ::1:8080...
* Connected to localhost (::1) port 8080 (#0)
> GET /check/e-tag HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.74.0
> Accept: */*
> IF-NONE-MATCH: "1234",
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 304 Not Modified
< etag: "1234"
< 
* Connection #0 to host localhost left intact
```

This one returns the expected result.

Referring to https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag it seems valid to not send trailing comma. Am I missing something?

### Do you have a reproducer?

https://github.com/FrederikS/vertx-etag-none-match-reproducer


Changed files:
vertx-web/src/test/java/io/vertx/ext/web/RouterTest.java, vertx-web/src/main/java/io/vertx/ext/web/impl/Utils.java

diff --git a/vertx-web/src/main/java/io/vertx/ext/web/impl/Utils.java b/vertx-web/src/main/java/io/vertx/ext/web/impl/Utils.java
index 2425fe96b..572f02319 100644
--- a/vertx-web/src/main/java/io/vertx/ext/web/impl/Utils.java
+++ b/vertx-web/src/main/java/io/vertx/ext/web/impl/Utils.java
@@ -161,7 +161,8 @@ public class Utils extends io.vertx.core.impl.Utils {
       int end = 0;
       int start = 0;
 
-      loop: for (int i = 0; i < noneMatch.length(); i++) {
+      loop:
+      for (int i = 0; i < noneMatch.length(); i++) {
         switch (noneMatch.charAt(i)) {
           case ' ':
             if (start == end) {
@@ -183,7 +184,11 @@ public class Utils extends io.vertx.core.impl.Utils {
       }
 
       if (etagStale) {
-        return false;
+        // the parser run out of bytes, need to check if the match is valid
+        String match = noneMatch.substring(start, end);
+        if (!match.equals(etag) && !match.equals("W/" + etag) && !("W/" + match).equals(etag)) {
+          return false;
+        }
       }
     }
 
@@ -196,7 +201,7 @@ public class Utils extends io.vertx.core.impl.Utils {
 
       boolean modifiedStale = lastModified == -1 || !(lastModified <= parseRFC1123DateTime(modifiedSince));
 
-        return !modifiedStale;
+      return !modifiedStale;
     }
 
     return true;
diff --git a/vertx-web/src/test/java/io/vertx/ext/web/RouterTest.java b/vertx-web/src/test/java/io/vertx/ext/web/RouterTest.java
index 590602dae..1f3db5547 100644
--- a/vertx-web/src/test/java/io/vertx/ext/web/RouterTest.java
+++ b/vertx-web/src/test/java/io/vertx/ext/web/RouterTest.java
@@ -2904,4 +2904,25 @@ public class RouterTest extends WebTestBase {
 
     testRequest(HttpMethod.GET, "/?u=" + utf8 + "&l=" + latin1, 200, "OK");
   }
+
+  @Test
+  public void testETag() throws Exception {
+    router.route("/check/e-tag").handler(ctx -> {
+      ctx.etag("1234");
+
+      if (ctx.isFresh()) {
+        ctx.response().setStatusCode(304).end("Not Modified");
+      } else {
+        ctx.response().setStatusCode(200).end("OK");
+      }
+    });
+
+    testRequest(HttpMethod.GET, "/check/e-tag", req -> {
+      req.putHeader("IF-NONE-MATCH", "\\"1234\\",");
+    }, 304, "Not Modified", "");
+
+    testRequest(HttpMethod.GET, "/check/e-tag", req -> {
+      req.putHeader("IF-NONE-MATCH", "\\"1234\\"");
+    }, 304, "Not Modified", "");
+  }
 }