Search code examples
springcontent-type

Why would spring overwrite Content-Type in header?


After I set http header in Controller in such way:

@Controller
@Slf4j
public class PlayerController {    
    @ModelAttribute
    public void setVaryResponseHeader(HttpServletResponse response) {
        response.setHeader("Content-Type", "application/vnd.apple.mpegurl");
    }

    @ResponseBody
    @RequestMapping(value = "/live/timeshift.m3u8", method = RequestMethod.GET)
    public String playbackLive(@RequestParam(value = "delay") Integer delay) {
        ....
    }
}

then later Spring overwrite it to plain text, callstack here:

writeWithMessageConverters:184, AbstractMessageConverterMethodProcessor (org.springframework.web.servlet.mvc.method.annotation)
handleReturnValue:174, RequestResponseBodyMethodProcessor (org.springframework.web.servlet.mvc.method.annotation)
handleReturnValue:81, HandlerMethodReturnValueHandlerComposite (org.springframework.web.method.support)
......
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

why would spring do that? it's so hard to understand, I think user customized logic has higher priority, spring should not make decision automatically.

actually, I think the question has enough explanation, but SO thinks there is mostly code, asking to add more detail, what can I do, thanks in advance.


Solution

  • TL;DR

    Why and how spring overwrite the ContentType in header?

    There is a default spring configuration, based on 3 strategies which select the content type to be returned. The configuration could be modified.

    Customising all the content-type header responses from all the controllers

    There are two ways of customising the content-type header negotiation configuration for all the responses, by XML configuration and another via Annotation driven configuration.

    Customising the content-type header value for certain URLs

    On very same configuration, there is a way of injection a customised strategy for choosing which URLs should be affected by the rule of changing the content-type header.

    On Spring Boot

    Thankfully, on spring boot adding the produce attribute on the @RequestMapping anotation of the controller and also changing a property would be enough for getting the desire behaviour:

    • @RequestMapping(value = "/live/timeshift.m3u8", method = RequestMethod.GET, produces = "application/vnd.apple.mpegurl")
    • Property - > spring.http.encoding.enabled set to true

    Long Answer

    Why and how spring overwrite the ContentType in header?

    In Spring MVC there are three options to determine the media type of a request:

    1. URL suffixes (extensions) in the request (like .xml/.json)
    2. URL parameter in the request (like ?format=json)
    3. Accept header in the request done to your controller method

    On this very order Spring negotiate the content-type header response and the body response format, and if none of these are enabled, we can specify a fallback to a default content type.

    Customising all the content-type header responses from all the controllers

    So for customising this behavior we should provide a fallback default content type and deactivate the three strategies described above. There are two approaches for acomplishing it, using XML configuration or annotation configuration:

    @Configuration
    @EnableWebMvc
    public class WebConfig extends WebMvcConfigurerAdapter {
    
      @Override
      public void configureContentNegotiation(final ContentNegotiationConfigurer configurer) {
        configurer.favorPathExtension(false).
        favorParameter(false).
        ignoreAcceptHeader(true).
        useJaf(false).
        defaultContentType("application/vnd.apple.mpegurl");
      }
    }
    

    Or the XML configuration

    <bean id="contentNegotiationManager"
      class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
        <property name="favorPathExtension" value="false" />
        <property name="favorParameter" value="false"/>
        <property name="ignoreAcceptHeader" value="true" />
        <property name="defaultContentType" value="application/vnd.apple.mpegurl"/>
        <property name="useJaf" value="false" />
    </bean>
    

    Customising the content-type header value for certain URLs

    Another custom solution would be create your own @DefaultContentType annotation. Overrinding RequestMappingHandlerMapping#getCustomMethodCondition which checks for the annotation@DefaultContentType on the method. The custom condition would always match but in the compareTo, it would prioritize methods that have the annotation, over those that don't.

    I would do the above solution if you need to use it a lot of times.

    For a one off occurrence, you could plug in a custom defaultContentTypeStrategy via ContentNegotiationConfigurer that checks for a specific URL and returns a preferred media type like:

    public class MyCustomContentNegotiationStrategy implements ContentNegotiationStrategy {
    
      @Override
      public List<MediaType> resolveMediaTypes (final NativeWebRequest nativeWebRequest)
                throws HttpMediaTypeNotAcceptableException {
          final List<MediaType> mediaTypes = new ArrayList<>();
          final String url =((ServletWebRequest)request).getRequest().getRequestURI().toString();
          final String yourUrlpatternString = ".*http://.*";
    
          final Pattern yourUrlPattern = Pattern.compile(patternString);
          final Matcher matcher = pattern.matcher(url);
    
         if(matcher.matches()) {
              mediaTypes.add("application/vnd.apple.mpegurl");
          return mediaTypes;
      }
    }
    

    Then, add your custom strategy via configuration:

    @EnableWebMvc
    @Configuration
    public class MyWebConfig extends WebMvcConfigurerAdapter {
    
      @Override
      public void configureContentNegotiation (ContentNegotiationConfigurer configurer) {
          configurer.defaultContentTypeStrategy(new MyCustomContentNegotiationStrategy());
      }
    }
    

    On Spring Boot

    Finally, if you're using spring boot, as @StavShamir has suggested, on answer https://stackoverflow.com/a/62422889/3346298, there are a bunch of common application properties which could be helpful on this case:

    # HTTP encoding (HttpEncodingProperties)
    # Charset of HTTP requests and responses. Added to the "Content-Type" header if not set explicitly.
    spring.http.encoding.charset=UTF-8 
    # Whether to enable http encoding support.
    spring.http.encoding.enabled=true 
    # Whether to force the encoding to the configured charset on HTTP requests and responses.
    spring.http.encoding.force= 
    # Whether to force the encoding to the configured charset on HTTP requests. Defaults to true when "force" has not been 
    spring.http.encoding.force-request= specified.
    # Whether to force the encoding to the configured charset on HTTP responses.
    spring.http.encoding.force-response= 
    # Locale in which to encode mapping.
    spring.http.encoding.mapping= 
    

    https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#common-application-properties

    On this very case, the spring.http.encoding.enabled property set to true and using the argument produces argument on @RequestMapping annotation would work:

    @RequestMapping(value = "/live/timeshift.m3u8", method = RequestMethod.GET, produces = "application/vnd.apple.mpegurl")