Repository: quarkusio/quarkus
Issue: Bad JSON returned from the default QuarkusErrorServlet

**Describe the bug**
Using quarkus-undertow and quarkus-resteasy, if an exception is thrown in a JAX-RS endpoint and no ExceptionMapper catches it gets forwarded to QuarkusErrorServlet. 

In case of JSON Accept header the stack does not get properly escaped there.

For example, I've encountered a stack ending with (there's a tab before ....):
`(SynchronousDispatcher.java:488)\\n	... 53 more`
which produces a JSON with a string including a tab delimiter.

Here is an example of a more thorough escaping from JSON simple.
https://github.com/fangyidong/json-simple/blob/master/src/main/java/org/json/simple/JSONValue.java#L270


**Environment:**
 - Quarkus version 1.4.2.Final


Changed files:
extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/QuarkusErrorServlet.java

diff --git a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/QuarkusErrorServlet.java b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/QuarkusErrorServlet.java
index 8f109706100..5e2046ef91e 100644
--- a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/QuarkusErrorServlet.java
+++ b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/QuarkusErrorServlet.java
@@ -40,9 +40,10 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) throws
         if (accept != null && accept.contains("application/json")) {
             resp.setContentType("application/json");
             resp.setCharacterEncoding(StandardCharsets.UTF_8.name());
-            String escapedStack = stack.replace(System.lineSeparator(), "\\\\n").replace("\\"", "\\\\\\"");
-            StringBuilder jsonPayload = new StringBuilder("{\\"details\\":\\"").append(details).append("\\",\\"stack\\":\\"")
-                    .append(escapedStack).append("\\"}");
+            String escapedDetails = escapeJsonString(details);
+            String escapedStack = escapeJsonString(stack);
+            StringBuilder jsonPayload = new StringBuilder("{\\"details\\":\\"").append(escapedDetails)
+                    .append("\\",\\"stack\\":\\"").append(escapedStack).append("\\"}");
             resp.getWriter().write(jsonPayload.toString());
         } else {
             //We default to HTML representation
@@ -60,12 +61,12 @@ private static String generateStackTrace(final Throwable exception) {
         StringWriter stringWriter = new StringWriter();
         exception.printStackTrace(new PrintWriter(stringWriter));
 
-        return escapeHtml(stringWriter.toString().trim());
+        return stringWriter.toString().trim();
     }
 
     private static String generateHeaderMessage(final Throwable exception, String uuid) {
-        return escapeHtml(String.format("Error handling %s, %s: %s", uuid, exception.getClass().getName(),
-                extractFirstLine(exception.getMessage())));
+        return String.format("Error handling %s, %s: %s", uuid, exception.getClass().getName(),
+                extractFirstLine(exception.getMessage()));
     }
 
     private static String extractFirstLine(final String message) {
@@ -77,14 +78,36 @@ private static String extractFirstLine(final String message) {
         return lines[0].trim();
     }
 
-    private static String escapeHtml(final String bodyText) {
-        if (bodyText == null) {
-            return "null";
+    private static String escapeJsonString(final String text) {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < text.length(); i++) {
+            char ch = text.charAt(i);
+            switch (ch) {
+                case '"':
+                    sb.append("\\\\\\"");
+                    break;
+                case '\\\\':
+                    sb.append("\\\\\\\\");
+                    break;
+                case '\\b':
+                    sb.append("\\\\b");
+                    break;
+                case '\\f':
+                    sb.append("\\\\f");
+                    break;
+                case '\\n':
+                    sb.append("\\\\n");
+                    break;
+                case '\\r':
+                    sb.append("\\\\r");
+                    break;
+                case '\\t':
+                    sb.append("\\\\t");
+                    break;
+                default:
+                    sb.append(ch);
+            }
         }
-
-        return bodyText
-                .replace("&", "&amp;")
-                .replace("<", "&lt;")
-                .replace(">", "&gt;");
+        return sb.toString();
     }
 }