Change ContentType or CharacterEncoding in Java Filter ONLY IF ContentType === JSON

13,884

Solution 1

Thanks to the other answers on this page, I found a way to do it.... Very close to what they were suggesting, but it turned out the only way I could get it to work was to override "getOutputStream" and look at the contentType at that point. I've put this filter as the first filter in the chain, and it seems to work fine.

import java.io.IOException;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.ws.rs.core.MediaType;

public class EnsureJsonIsUtf8ResponseFilter implements Filter
{
    final String APPLICATION_JSON_WITH_UTF8_CHARSET = MediaType.APPLICATION_JSON + ";charset=" + java.nio.charset.StandardCharsets.UTF_8;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
    {
        HttpServletResponse r = (HttpServletResponse) response;
        HttpServletResponse wrappedResponse = new HttpServletResponseWrapper(r) 
        {
            @Override
            public ServletOutputStream getOutputStream() throws java.io.IOException
            {
                ServletResponse response = this.getResponse();

                String ct = (response != null) ? response.getContentType() : null;
                if (ct != null && ct.toLowerCase().startsWith(MediaType.APPLICATION_JSON))
                {
                    response.setContentType(APPLICATION_JSON_WITH_UTF8_CHARSET);
                }

                return super.getOutputStream();
            }
        };

        chain.doFilter(request, wrappedResponse); 
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException
    {
        // This method intentionally left blank
    }

    @Override
    public void destroy()
    {
        // This method intentionally left blank
    }
}

Solution 2

This is not going to work in this way.

When you call chain.doFilter(request, response); your headers are already flushed and you can't reset them later on.

What you can do is actually a quick and dirty trick:

public void doFilter(...) {
    HttpServletResponse resp = new HttpServletResponseWrapper(response) {
    public void setContentType(String ct) {
        if(ct!=null && ct.toLowerCase().startsWith("application/json")) {
            super.setContentType("application/json;charset=UTF-8");
        } else {
            super.setContentType(ct);
        }
   }
}

// Set content type manually to override any potential defaults,
// See if you need it at all
response.setContentType("application/json;charset=UTF-8");

chain.doFilter(request, resp); // Inject our response!
}

EDIT: ct.toUpperCase().startsWith("application/json") changed to ct.toLowerCase().startsWith("application/json").

Solution 3

Using this answer as reference the solution to your question is to re-encode the JSON text as shown here:

public void doFilter(...) {
    final CharResponseWrapper wrappedResponse =
            new CharResponseWrapper((HttpServletResponse) response);

    chain.doFilter(request, wrappedResponse);

    final String content = wrappedResponse.toString();

    final String type = wrappedResponse.getContentType();
    if (type != null && type.contains(MediaType.APPLICATION_JSON)) {
        // Re-encode the JSON response as UTF-8.
        response.setCharacterEncoding("UTF-8");
        final OutputStream out = response.getOutputStream();
        out.write(content.getBytes("UTF-8"));
        out.close();
    }
    else {
        // Otherwise just write it as-is.
        final PrintWriter out = response.getWriter();
        out.write(content);
        out.close();
    }
}

Solution 4

This also may be doable using a ClientFilter, which I've just come across a StackOverflow post for a similar purpose:

https://stackoverflow.com/a/7464585/26510

Solution 5

Had success using a ContainerResponseFilter:

public class ContentTypeEncodingFilter implements ContainerResponseFilter {
    @Override
    public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
        String contentType = responseContext.getHeaderString(HttpHeaders.CONTENT_TYPE);
        if (contentType == null) {
            return;
        }
        ContentType parsedType = ContentType.parse(contentType);
        if (parsedType.getCharset() != null) {
            return;
        }
        ContentType encodedType = parsedType.withCharset(StandardCharsets.UTF_8);
        responseContext.getHeaders().putSingle(HttpHeaders.CONTENT_TYPE, encodedType.toString());
    }
}
Share:
13,884
Brad Parks
Author by

Brad Parks

Web programmer, interested in node js, cross platform development, and automating the things!

Updated on June 05, 2022

Comments

  • Brad Parks
    Brad Parks almost 2 years

    I'm trying to ensure that all JSON responses from a Jersey based java application have a UTF-8 character encoding parameter appended to their ContentType header.

    So if it's a JSON response, I would like the response header for the Content-Type to be

    Content-Type: application/json;charset=UTF-8

    EDIT: I know I can do this on a case by case basis, but I'd like to do it globally, so it affects all content responses that have a content type of "application/json".

    If I just try and set the character encoding in my filter regardless of the content type, it works fine. But I only want to set the character encoding if the ContentType is "application/json". I find that the response.getContentType() method always returns null unless I call chain.doFilter first. But if I try and change the Character Encoding after this, it seems to always get overwritten.

    import java.io.IOException;
    import javax.servlet.*;
    import javax.servlet.http.*;
    import javax.ws.rs.core.MediaType;
    
    public class EnsureJsonResponseIsUtf8Filter implements Filter
    {
        private class SimpleWrapper extends HttpServletResponseWrapper
        {
            public SimpleWrapper(HttpServletResponse response)
            {
                super(response);
            }
    
            @Override
            public String getCharacterEncoding()
            {
                return "UTF-8";
            }
        }
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
        {
            chain.doFilter(request, response);
    
            if (response.getContentType() != null && response.getContentType().contains(MediaType.APPLICATION_JSON))
            {
                response.setCharacterEncoding("UTF-8");
                chain.doFilter(request, new SimpleWrapper((HttpServletResponse) response));
            }
        }
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException
        {
        }
    
        @Override
        public void destroy()
        {
        }
    }
    

    I've seen other similar questions, but none them seem to have this issue. I've tried registering my filter as the first, and last filter with no luck.

  • Brad Parks
    Brad Parks about 10 years
    Hey... thanks... I'm trying to ensure that all responses that have a ContentType of "application/json" also response with a character set header of "UTF-*"....
  • Torindo
    Torindo about 10 years
    The easiest thing I can think of it to group all your json actions in a common path prefix and then in the filter you set the content-type based on the request path. For Instance if your hostname is foo.com, all your json actions have the following url prefix foo.com/json*. Now in the filter, before calling chain.doFilter you set the content-type if the url prefix is foo.com/json*
  • Brad Parks
    Brad Parks about 10 years
    hey! thanks for the idea.... it seems to work fine, and gets overridden by other responses as hoped (e.g Content-Type: image/jpeg).... kudos! note that i had to make sure this was the first filter in the chain to make it work, as expected....
  • Brad Parks
    Brad Parks about 10 years
    hmmm.... i thought it worked, but i can't seem to get it to work again.... I was in a bit of a rush to try out the solutions before the bounty ended, so maybe I saw something that wasn't there.... i did notice that in the code above, it says ct.toUpperCase().startsWith("application/json, which will never be true... so I'm guessing this wouldn't ever have worked... thanks for the suggestion... I'll report back if I find out anything different....
  • boky
    boky about 10 years
    Well, you said you just wanted to add charset. If you're doing something else, you could always catch it in the same way. How DO you know if the response is your JSON or not? By URL?
  • Brad Parks
    Brad Parks about 10 years
    Well I guess maybe that's part of the problem. The only way I know is by looking at the ContentType header, which only seems to have something in it (ie not null) after chain.doFilter has been called. I don't want to just set the charset to UTF-8 on all responses, as it causes problems with IE8 images from what I've read.
  • Arjun Chiddarwar
    Arjun Chiddarwar about 9 years
    I want to set Cache-Control to "no-cache, no-store, must-revlidate, max-age=0" in RESPONSE header if the content-type is "text/html" I'm blocked at this case. Can u help?
  • Brad Parks
    Brad Parks about 9 years
    hmm... seems like the same idea should work to me. Here's an untested guess at how this would go, and googling around, both this post and this additional post may answer your question