3

I'm making a DSL for embedded Jetty, and I'm having trouble setting characterEncoding and contentType. I want the users to be able to specify default values for these two fields, but Jetty is making life hard.

res.characterEncoding = null gives res.characterEncoding the value iso-8859-1.

res.characterEncoding = "", gives res.characterEncoding the value "", but res.contentType becomes application/json;charset=

res.characterEncoding = "" THEN res.characterEncoding = null has the same effect as just doing res.characterEncoding = ""

I ended up with a ridiculous code snippet based on this odd behavior:

if (res.characterEncoding.contains(";javalin-default") || res.contentType.contains(";javalin-default")) {
    res.contentType = res.contentType.replace(";javalin-default", "")
    res.characterEncoding = null
    if (res.contentType.contains("application/json")) {
        res.contentType = "application/json"
    } else {
        res.characterEncoding = defaultCharacterEncoding
    }
}

But that can't be the right thing to do. Any ideas?

I have an issue for it here: https://github.com/tipsy/javalin/issues/259

tipsy
  • 402
  • 4
  • 15

1 Answers1

7

You are fighting an overly complex aspect of the Servlet spec.

Opinion: The HttpServletResponse.setContentType(String) method should never have existed, it should have just been .setMimeType(String) and .setCharacterEncoding(Charset)

Lets start with how character encoding is important.

When HttpServletResponse.getWriter() is accessed, the implementation must resolve the response character encoding and locale to use for the created PrintWriter. This means that the character encoding at this point will have a value assigned.

Notice that the locale is used too, this is often overlooked, but since you are a library writer, this should be pointed out to you. See HttpServletResponse.setLocale(Locale) and HttpServletResponse.getLocale().

Something else to consider is that if you have accessed HttpServletResponse.getWriter() already, then the use of HttpServletResponse.setCharacterEncoding(String) later results in a no-op, and using HttpServletResponse.setContentType(String) with a charset can result in the charset being stripped from the header produced. (again, this is in line with Servlet spec behavior).

The HttpServletResponse.getCharacterEncoding() can return the character encoding if manually set to a valid value earlier, or based on the Content-Type if already declared, otherwise it defaults to ISO-8859-1. If it is using the Content-Type it first checks for charset parameter and uses it. If Content-Type does not have charset, then it uses the mime-type configuration in your web metadata. This call should never produce an empty or null character encoding.

Servlet Spec default for use of HttpServletResponse.getCharacterEncoding() is ISO-8859-1 (this value is taken from RFC2616, when this aspect of the Servlet spec was defined).

The web metadata comes from the web descriptor (aka the WEB-INF/web.xml), default descriptor, override descriptor, org/eclipse/jetty/http/mime.properties resource, org/eclipse/jetty/http/encoding.properties, other features present (such as GzipHandler) and programmatic configurations.

In Jetty, all of these various configuration sources for mime-type result in a configured Jetty org.eclipse.jetty.http.MimeTypes object.

When HttpServletResponse.setCharacterEncoding(String) is called, it also has the responsibility to modify the Content-Type field and header on the response.

Assuming that .getWriter() hasn't been called yet, using setCharacterEncoding(null) will remove any existing charset parameter on Content-Type field and header. setCharacterEncoding("utf-8") would add/change the charset parameter to utf-8 on the Content-Type field and header.

When HttpServletResponse.setContentType(String) is called, it also has the responsibility to modify the Character Encoding field if a charset parameter was provided.

Knowing all this, you'll realize that you have to be careful of the order of the various API calls, in particular when the HttpServletResponse.getWriter() call is made.

Instead of using the Servlet HttpServletResponse APIs to manage this, you should perhaps be controlling this via the web metadata for application/json on startup of the webapp.

In your case, you'd just configure the MimeTypes on the ServletContextHandler that your JettyServerUtil.kt is creating, no need for any of the hacks you are using.

Default behavior of Jetty's ServletContextHandler and MimeTypes configuration will not add the charset, due to how application/json is defined in the org/eclipse/jetty/http/encoding.properties resource (as an assumed charset).

Example:

package jetty;

import static java.nio.charset.StandardCharsets.UTF_8;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.IO;

public class MimeTypeJsonExample
{
    public static void main(String[] args) throws Exception
    {
        Server server = new Server(9090);
        ServletContextHandler context = new ServletContextHandler();
        context.setContextPath("/");
        context.addServlet(JsonServlet.class, "/demo");
        context.addServlet(DefaultServlet.class, "/"); // handle static content and errors for this context
        HandlerList handlers = new HandlerList();
        handlers.addHandler(context);
        handlers.addHandler(new DefaultHandler()); // handle non-context errors
        server.setHandler(context);
        server.start();

        try
        {
            demonstrateJsonBehavior(server.getURI().resolve("/"));
        }
        finally
        {
            server.stop();
        }
    }

    private static void demonstrateJsonBehavior(URI serverBaseUri) throws IOException
    {
        HttpURLConnection http = (HttpURLConnection) serverBaseUri.resolve("/demo").toURL().openConnection();
        dumpRequestResponse(http);
        System.out.println();
        try (InputStream in = http.getInputStream())
        {
            System.out.println(IO.toString(in, UTF_8));
        }
    }

    private static void dumpRequestResponse(HttpURLConnection http) throws IOException
    {
        System.out.println();
        System.out.println("----");
        System.out.printf("%s %s HTTP/1.1%n", http.getRequestMethod(), http.getURL());
        System.out.println("----");
        System.out.printf("%s%n", http.getHeaderField(null));
        http.getHeaderFields().entrySet().stream()
                .filter(entry -> entry.getKey() != null)
                .forEach((entry) -> System.out.printf("%s: %s%n", entry.getKey(), http.getHeaderField(entry.getKey())));
    }

    public static class JsonServlet extends HttpServlet
    {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
        {
            resp.setContentType("application/json");
            PrintWriter writer = resp.getWriter();
            resp.setHeader("X-Charset", resp.getCharacterEncoding());
            writer.println("{\"mode\":[\"a=b\"],\"animals\":[[\"kiwi bird\",\"kea\",\"skink\"]]}");
        }
    }
}

This results in the output ...

2018-06-27 09:00:32.754:INFO::main: Logging initialized @360ms to org.eclipse.jetty.util.log.StdErrLog
2018-06-27 09:00:32.898:INFO:oejs.Server:main: jetty-9.4.11.v20180605; built: 2018-06-05T18:24:03.829Z; git: d5fc0523cfa96bfebfbda19606cad384d772f04c; jvm 9.0.4+11
2018-06-27 09:00:32.969:INFO:oejsh.ContextHandler:main: Started o.e.j.s.ServletContextHandler@5dd6264{/,null,AVAILABLE}
2018-06-27 09:00:33.150:INFO:oejs.AbstractConnector:main: Started ServerConnector@60707857{HTTP/1.1,[http/1.1]}{0.0.0.0:9090}
2018-06-27 09:00:33.151:INFO:oejs.Server:main: Started @764ms

----
GET http://192.168.0.119:9090/demo HTTP/1.1
----
HTTP/1.1 200 OK
Server: Jetty(9.4.11.v20180605)
X-Charset: utf-8
Content-Length: 58
Date: Wed, 27 Jun 2018 14:00:33 GMT
Content-Type: application/json

{"mode":["a=b"],"animals":[["kiwi bird","kea","skink"]]}

2018-06-27 09:00:33.276:INFO:oejs.AbstractConnector:main: Stopped ServerConnector@60707857{HTTP/1.1,[http/1.1]}{0.0.0.0:9090}
2018-06-27 09:00:33.278:INFO:oejsh.ContextHandler:main: Stopped o.e.j.s.ServletContextHandler@5dd6264{/,null,UNAVAILABLE}

As you can see, the JsonServlet only set the Content-Type mime-type, accessed the PrintWriter, set a header X-Charset to show the current character encoding value, and then wrote the json content.

The Content-Type response header seen by the client does not include the assumed charset of utf-8.

Joakim Erdfelt
  • 46,896
  • 7
  • 86
  • 136