Repository: quarkusio/quarkus
Issue: Utf-8 graphql issue with non ascii chars


## Describe the bug
Utf-8 chars like german umlauts are not usable.

### Expected behavior
Special chars should work like normal chars.

### Actual behavior
Special chars like german ä, ö, ü are replaced by things like Ã¼


## To Reproduce

Or attach an archive containing the reproducer to the issue.
[code-with-quarkus_graphql_utf8.zip](https://github.com/quarkusio/quarkus/files/6783786/code-with-quarkus_graphql_utf8.zip)

Steps to reproduce the behavior:
1. Send the example query via dev ui or intellij http request.
2. The response does contain destroyed umlauts.

```
Example Query:

query test{
  hello(string: "München")
}

Example response:

{
  "data": {
    "hello": "MÃ¼nchen"
  }
}

Example Intellij http request:

POST http://localhost:8080/graphql
Content-Type: application/json

{"query":"query test{\\n  hello(string: \\"München\\")\\n}\\n","variables":null,"operationName":"test"}


### Output of `uname -a` or `ver`

### Output of `java -version`
openjdk version "11.0.11" 2021-04-20
OpenJDK Runtime Environment AdoptOpenJDK-11.0.11+9 (build 11.0.11+9)
OpenJDK 64-Bit Server VM AdoptOpenJDK-11.0.11+9 (build 11.0.11+9, mixed mode)
### Quarkus version or git rev
2.0.1.Final

Problem could be here:

io.quarkus.smallrye.graphql.runtime.SmallRyeGraphQLExecutionHandler#readBody

return new String(bytes);

Changed files:
extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/TestResource.java
extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLAbstractHandler.java
extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLCDIContextPropagationTest.java
extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/AbstractGraphQLTest.java
extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLExecutionHandler.java
extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLContextTestCase.java
extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLTest.java

diff --git a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/AbstractGraphQLTest.java b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/AbstractGraphQLTest.java
index fe1f8f6e67c..ba8dd7e1769 100644
--- a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/AbstractGraphQLTest.java
+++ b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/AbstractGraphQLTest.java
@@ -14,12 +14,17 @@
 import org.hamcrest.CoreMatchers;
 
 import io.restassured.RestAssured;
+import io.restassured.parsing.Parser;
 
 /**
  * Some shared methods
  */
 public abstract class AbstractGraphQLTest {
 
+    static {
+        RestAssured.registerParser("application/graphql+json", Parser.JSON);
+    }
+
     protected void pingTest() {
         pingPongTest("ping", "pong");
     }
diff --git a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLCDIContextPropagationTest.java b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLCDIContextPropagationTest.java
index 7535660ca60..fed77846f4f 100644
--- a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLCDIContextPropagationTest.java
+++ b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLCDIContextPropagationTest.java
@@ -25,7 +25,6 @@
 
 import io.quarkus.test.QuarkusUnitTest;
 import io.restassured.RestAssured;
-import io.restassured.http.ContentType;
 
 /**
  * Testing scenarios which require CDI context propagation to work under the hood.
@@ -51,8 +50,8 @@ public void testCdiContextPropagationForBatchSources() {
                 "}");
 
         RestAssured.given().when()
-                .accept(ContentType.JSON)
-                .contentType(ContentType.JSON)
+                .accept(MEDIATYPE_JSON)
+                .contentType(MEDIATYPE_JSON)
                 .body(pingRequest)
                 .post("/graphql")
                 .then()
@@ -74,8 +73,8 @@ public void testCdiContextPropagationForBatchSourcesAsync() {
                 "}");
 
         RestAssured.given().when()
-                .accept(ContentType.JSON)
-                .contentType(ContentType.JSON)
+                .accept(MEDIATYPE_JSON)
+                .contentType(MEDIATYPE_JSON)
                 .body(pingRequest)
                 .post("/graphql")
                 .then()
@@ -98,8 +97,8 @@ public void testManualContextPropagation() {
                 "}");
 
         RestAssured.given().when()
-                .accept(ContentType.JSON)
-                .contentType(ContentType.JSON)
+                .accept(MEDIATYPE_JSON)
+                .contentType(MEDIATYPE_JSON)
                 .body(pingRequest)
                 .post("/graphql")
                 .then()
diff --git a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLTest.java b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLTest.java
index b78f7b226a3..d380916f6c9 100644
--- a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLTest.java
+++ b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLTest.java
@@ -2,6 +2,7 @@
 
 import static io.quarkus.smallrye.graphql.deployment.AbstractGraphQLTest.MEDIATYPE_JSON;
 
+import java.nio.charset.Charset;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -106,6 +107,67 @@ public void testSourcePost() {
 
     }
 
+    @Test
+    public void testWrongAcceptType() {
+        String fooRequest = getPayload("{\\n" +
+                "  foo {\\n" +
+                "    message\\n" +
+                "    randomNumber{\\n" +
+                "       value\\n" +
+                "    }\\n" +
+                "    list\\n" +
+                "  }\\n" +
+                "}");
+
+        RestAssured.given().when()
+                .accept(MEDIATYPE_TEXT)
+                .contentType(MEDIATYPE_JSON)
+                .body(fooRequest)
+                .post("/graphql")
+                .then()
+                .assertThat()
+                .statusCode(406);
+    }
+
+    @Test
+    public void testUTF8Charset() {
+        String fooRequest = getPayload("{\\n" +
+                "  testCharset(characters:\\"óôöúüýáâäçéëíî®©\\")\\n" +
+                "}");
+
+        byte[] response = RestAssured.given().when()
+                .body(fooRequest)
+                .post("/graphql")
+                .then()
+                .assertThat()
+                .statusCode(200)
+                .and()
+                .extract().body().asByteArray();
+
+        String decodedResponse = new String(response, Charset.forName("UTF-8"));
+        Assertions.assertTrue(decodedResponse.contains("{\\"data\\":{\\"testCharset\\":\\"óôöúüýáâäçéëíî®©\\"}}"));
+    }
+
+    @Test
+    public void testCP1250Charset() {
+        String fooRequest = getPayload("{\\n" +
+                "  testCharset(characters:\\"óôöúüýáâäçéëíî®©\\")\\n" +
+                "}");
+
+        byte[] response = RestAssured.given().when()
+                .accept("application/json;charset=CP1250")
+                .body(fooRequest)
+                .post("/graphql")
+                .then()
+                .assertThat()
+                .statusCode(200)
+                .and()
+                .extract().body().asByteArray();
+
+        String decodedResponse = new String(response, Charset.forName("CP1250"));
+        Assertions.assertTrue(decodedResponse.contains("{\\"data\\":{\\"testCharset\\":\\"óôöúüýáâäçéëíî®©\\"}}"));
+    }
+
     @Test
     public void testSourcePost2() {
         String foosRequest = getPayload("{\\n" +
diff --git a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLContextTestCase.java b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLContextTestCase.java
index 0d117b54bb5..2f3acec11fb 100644
--- a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLContextTestCase.java
+++ b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLContextTestCase.java
@@ -26,7 +26,6 @@
 
 import io.quarkus.test.QuarkusUnitTest;
 import io.restassured.RestAssured;
-import io.restassured.http.ContentType;
 import io.smallrye.graphql.api.Context;
 
 public class SmallRyeGraphQLContextTestCase extends AbstractGraphQLTest {
@@ -45,8 +44,8 @@ public void testAsyncQuery() {
                 "}");
 
         RestAssured.given().when()
-                .accept(ContentType.JSON)
-                .contentType(ContentType.JSON)
+                .accept(MEDIATYPE_JSON)
+                .contentType(MEDIATYPE_JSON)
                 .body(request)
                 .post("/graphql")
                 .then()
@@ -68,8 +67,8 @@ public void testAsyncQueryWithAsyncSource() {
                 "}");
 
         RestAssured.given().when()
-                .accept(ContentType.JSON)
-                .contentType(ContentType.JSON)
+                .accept(MEDIATYPE_JSON)
+                .contentType(MEDIATYPE_JSON)
                 .body(request)
                 .post("/graphql")
                 .then()
@@ -91,8 +90,8 @@ public void testManualPropagation() {
                 "}");
 
         RestAssured.given().when()
-                .accept(ContentType.JSON)
-                .contentType(ContentType.JSON)
+                .accept(MEDIATYPE_JSON)
+                .contentType(MEDIATYPE_JSON)
                 .body(request)
                 .post("/graphql")
                 .then()
@@ -114,8 +113,8 @@ public void testSourceMethods() {
                 "}");
 
         RestAssured.given().when()
-                .accept(ContentType.JSON)
-                .contentType(ContentType.JSON)
+                .accept(MEDIATYPE_JSON)
+                .contentType(MEDIATYPE_JSON)
                 .body(request)
                 .post("/graphql")
                 .then()
diff --git a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/TestResource.java b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/TestResource.java
index e260e19c0b3..d33d00a97ea 100644
--- a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/TestResource.java
+++ b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/TestResource.java
@@ -61,6 +61,11 @@ public TestPojo moo(String name) {
         return new TestPojo(name);
     }
 
+    @Query
+    public String testCharset(String characters) {
+        return characters;
+    }
+
     // <placeholder>
 
     public TestRandom getRandomNumber(@Source TestPojo testPojo) {
diff --git a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLAbstractHandler.java b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLAbstractHandler.java
index 648dfdb1d55..5441b0d7a8d 100644
--- a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLAbstractHandler.java
+++ b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLAbstractHandler.java
@@ -3,7 +3,6 @@
 import java.io.StringReader;
 
 import javax.json.Json;
-import javax.json.JsonBuilderFactory;
 import javax.json.JsonObject;
 import javax.json.JsonReader;
 import javax.json.JsonReaderFactory;
@@ -28,8 +27,7 @@ public abstract class SmallRyeGraphQLAbstractHandler implements Handler<RoutingC
 
     private volatile ExecutionService executionService;
 
-    private static final JsonBuilderFactory jsonObjectFactory = Json.createBuilderFactory(null);
-    private static final JsonReaderFactory jsonReaderFactory = Json.createReaderFactory(null);
+    protected static final JsonReaderFactory jsonReaderFactory = Json.createReaderFactory(null);
 
     public SmallRyeGraphQLAbstractHandler(
             CurrentIdentityAssociation currentIdentityAssociation,
diff --git a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLExecutionHandler.java b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLExecutionHandler.java
index 99af8227e95..3f59874e910 100644
--- a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLExecutionHandler.java
+++ b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLExecutionHandler.java
@@ -4,14 +4,13 @@
 import java.io.StringReader;
 import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
 import java.util.List;
 
 import javax.json.Json;
-import javax.json.JsonBuilderFactory;
 import javax.json.JsonObject;
 import javax.json.JsonObjectBuilder;
 import javax.json.JsonReader;
-import javax.json.JsonReaderFactory;
 
 import io.quarkus.security.identity.CurrentIdentityAssociation;
 import io.quarkus.vertx.http.runtime.CurrentVertxRequest;
@@ -34,9 +33,10 @@ public class SmallRyeGraphQLExecutionHandler extends SmallRyeGraphQLAbstractHand
     private static final String EXTENSIONS = "extensions";
     private static final String APPLICATION_GRAPHQL = "application/graphql";
     private static final String OK = "OK";
-
-    private static final JsonBuilderFactory jsonObjectFactory = Json.createBuilderFactory(null);
-    private static final JsonReaderFactory jsonReaderFactory = Json.createReaderFactory(null);
+    private static final String DEFAULT_RESPONSE_CONTENT_TYPE = "application/graphql+json; charset="
+            + StandardCharsets.UTF_8.name();
+    private static final String DEFAULT_REQUEST_CONTENT_TYPE = "application/json; charset="
+            + StandardCharsets.UTF_8.name();
 
     public SmallRyeGraphQLExecutionHandler(boolean allowGet, boolean allowPostWithQueryParameters,
             CurrentIdentityAssociation currentIdentityAssociation,
@@ -48,25 +48,33 @@ public SmallRyeGraphQLExecutionHandler(boolean allowGet, boolean allowPostWithQu
 
     @Override
     protected void doHandle(final RoutingContext ctx) {
-
         HttpServerRequest request = ctx.request();
         HttpServerResponse response = ctx.response();
 
-        response.headers().set(HttpHeaders.CONTENT_TYPE, "application/json; charset=UTF-8");
+        String accept = getRequestAccept(ctx);
+        String requestedCharset = getCharset(accept);
+
+        boolean isValid = isValidAcceptRequest(accept);
 
-        switch (request.method().name()) {
-            case "OPTIONS":
-                handleOptions(response);
-                break;
-            case "POST":
-                handlePost(response, ctx);
-                break;
-            case "GET":
-                handleGet(response, ctx);
-                break;
-            default:
-                ctx.next();
-                break;
+        if (!isValid) {
+            handleInvalidAcceptRequest(response);
+        } else {
+            response.headers().set(HttpHeaders.CONTENT_TYPE, accept);
+
+            switch (request.method().name()) {
+                case "OPTIONS":
+                    handleOptions(response);
+                    break;
+                case "POST":
+                    handlePost(response, ctx, requestedCharset);
+                    break;
+                case "GET":
+                    handleGet(response, ctx, requestedCharset);
+                    break;
+                default:
+                    ctx.next();
+                    break;
+            }
         }
     }
 
@@ -75,7 +83,7 @@ private void handleOptions(HttpServerResponse response) {
         response.setStatusCode(200).setStatusMessage(OK).end();
     }
 
-    private void handlePost(HttpServerResponse response, RoutingContext ctx) {
+    private void handlePost(HttpServerResponse response, RoutingContext ctx, String requestedCharset) {
         try {
             JsonObject jsonObjectFromBody = getJsonObjectFromBody(ctx);
             String postResponse;
@@ -87,13 +95,13 @@ private void handlePost(HttpServerResponse response, RoutingContext ctx) {
             } else {
                 postResponse = doRequest(jsonObjectFromBody);
             }
-            response.setStatusCode(200).setStatusMessage(OK).end(Buffer.buffer(postResponse));
+            response.setStatusCode(200).setStatusMessage(OK).end(Buffer.buffer(postResponse, requestedCharset));
         } catch (IOException ex) {
             throw new RuntimeException(ex);
         }
     }
 
-    private void handleGet(HttpServerResponse response, RoutingContext ctx) {
+    private void handleGet(HttpServerResponse response, RoutingContext ctx, String requestedCharset) {
         if (allowGet) {
             try {
                 JsonObject input = getJsonObjectFromQueryParameters(ctx);
@@ -102,7 +110,7 @@ private void handleGet(HttpServerResponse response, RoutingContext ctx) {
                     String getResponse = doRequest(input);
                     response.setStatusCode(200)
                             .setStatusMessage(OK)
-                            .end(Buffer.buffer(getResponse));
+                            .end(Buffer.buffer(getResponse, requestedCharset));
 
                 } else {
                     response.setStatusCode(204).end();
@@ -115,30 +123,34 @@ private void handleGet(HttpServerResponse response, RoutingContext ctx) {
         }
     }
 
+    private void handleInvalidAcceptRequest(HttpServerResponse response) {
+        response.setStatusCode(406).end();
+    }
+
     private JsonObject getJsonObjectFromQueryParameters(RoutingContext ctx) throws UnsupportedEncodingException {
         JsonObjectBuilder input = Json.createObjectBuilder();
         // Query
         String query = stripNewlinesAndTabs(readQueryParameter(ctx, QUERY));
         if (query != null && !query.isEmpty()) {
-            input.add(QUERY, URLDecoder.decode(query, "UTF8"));
+            input.add(QUERY, URLDecoder.decode(query, StandardCharsets.UTF_8.name()));
         }
         // OperationName
         String operationName = readQueryParameter(ctx, OPERATION_NAME);
         if (operationName != null && !operationName.isEmpty()) {
-            input.add(OPERATION_NAME, URLDecoder.decode(query, "UTF8"));
+            input.add(OPERATION_NAME, URLDecoder.decode(query, StandardCharsets.UTF_8.name()));
         }
 
         // Variables
         String variables = stripNewlinesAndTabs(readQueryParameter(ctx, VARIABLES));
         if (variables != null && !variables.isEmpty()) {
-            JsonObject jsonObject = toJsonObject(URLDecoder.decode(variables, "UTF8"));
+            JsonObject jsonObject = toJsonObject(URLDecoder.decode(variables, StandardCharsets.UTF_8.name()));
             input.add(VARIABLES, jsonObject);
         }
 
         // Extensions
         String extensions = stripNewlinesAndTabs(readQueryParameter(ctx, EXTENSIONS));
         if (extensions != null && !extensions.isEmpty()) {
-            JsonObject jsonObject = toJsonObject(URLDecoder.decode(extensions, "UTF8"));
+            JsonObject jsonObject = toJsonObject(URLDecoder.decode(extensions, StandardCharsets.UTF_8.name()));
             input.add(EXTENSIONS, jsonObject);
         }
 
@@ -147,7 +159,7 @@ private JsonObject getJsonObjectFromQueryParameters(RoutingContext ctx) throws U
 
     private JsonObject getJsonObjectFromBody(RoutingContext ctx) throws IOException {
 
-        String contentType = readContentType(ctx);
+        String contentType = getRequestContentType(ctx);
         String body = stripNewlinesAndTabs(readBody(ctx));
 
         // If the content type is application/graphql, the query is in the body
@@ -167,18 +179,43 @@ private JsonObject getJsonObjectFromBody(RoutingContext ctx) throws IOException
 
     private String readBody(RoutingContext ctx) {
         if (ctx.getBody() != null) {
-            byte[] bytes = ctx.getBody().getBytes();
-            return new String(bytes);
+            return ctx.getBodyAsString();
         }
         return null;
     }
 
-    private String readContentType(RoutingContext ctx) {
+    private String getRequestContentType(RoutingContext ctx) {
         String contentType = ctx.request().getHeader("Content-Type");
-        if (contentType != null && !contentType.isEmpty()) {
+        if (contentType != null && !contentType.isEmpty() && !contentType.startsWith("*/*")) {
             return contentType;
         }
-        return null;
+        return DEFAULT_REQUEST_CONTENT_TYPE;
+    }
+
+    private String getRequestAccept(RoutingContext ctx) {
+        String accept = ctx.request().getHeader("Accept");
+        if (accept != null && !accept.isEmpty() && !accept.startsWith("*/*")) {
+            return accept;
+        }
+        return DEFAULT_RESPONSE_CONTENT_TYPE;
+    }
+
+    private String getCharset(String mimeType) {
+        if (mimeType != null && mimeType.contains(";")) {
+            String[] parts = mimeType.split(";");
+            for (String part : parts) {
+                if (part.trim().startsWith("charset")) {
+                    return part.split("=")[1];
+                }
+            }
+        }
+        return StandardCharsets.UTF_8.name();
+    }
+
+    private boolean isValidAcceptRequest(String mimeType) {
+        // At this point we only accept two 
+        return mimeType.startsWith("application/json")
+                || mimeType.startsWith("application/graphql+json");
     }
 
     private String readQueryParameter(RoutingContext ctx, String parameterName) {