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
.