0
votes

TL:DR

How can we override the current behavior of Spring 4.3+ that forces the use of the RequestMethod.GET or @GetMapping for HEAD requests so we can return the Content-Length header without having to write all the data to the responses OutputStream?


Longer Version:

It has just come to my attention that Spring has changed the way that GET/HEAD requests are handled by default:

HTTP HEAD, OPTIONS

@GetMapping — and also @RequestMapping(method=HttpMethod.GET), support HTTP HEAD transparently for request mapping purposes. Controller methods don’t need to change. A response wrapper, applied in javax.servlet.http.HttpServlet, ensures a "Content-Length" header is set to the number of bytes written and without actually writing to the response.

@GetMapping — and also @RequestMapping(method=HttpMethod.GET), are implicitly mapped to and also support HTTP HEAD. An HTTP HEAD request is processed as if it were HTTP GET except but instead of writing the body, the number of bytes are counted and the "Content-Length" header set.

By default HTTP OPTIONS is handled by setting the "Allow" response header to the list of HTTP methods listed in all @RequestMapping methods with matching URL patterns.

For a @RequestMapping without HTTP method declarations, the "Allow" header is set to "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS". Controller methods should always declare the supported HTTP methods for example by using the HTTP method specific variants — @GetMapping, @PostMapping, etc.

@RequestMapping method can be explicitly mapped to HTTP HEAD and HTTP OPTIONS, but that is not necessary in the common case.

Sources:
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-ann-requestmapping-head-options
https://stackoverflow.com/a/45412434/42962

How can we override this default behavior so we can handle the HEAD response and set the Content-Length header ourselves?

We want to do this because we sever large files (think over 10 gigs in size) through our web application we would like to not have to read all the bytes into the Response's OutputStream if possible.

Here is an example of our current code. Only the second method (handleRequest with the RequestMethod.GET) gets called.

@RequestMapping(value = "/file/{fileName:.+}", method = RequestMethod.HEAD)
public void handleHeadRequest(@RequestParam(value = "fileName") String fileName, HttpServletRequest request, HttpServletResponse response) {

    File file = fileRepository.getFileByName(fileName)
    response.addHeader("Accept-Ranges", "bytes");
    response.addDateHeader("Last-Modified", file.lastModified());

    Long fileSize = file.length();
    response.addHeader(HttpHeaderConstants.CONTENT_LENGTH, fileSize.toString());
}

@RequestMapping(value = "/file/{fileName:.+}", headers = "!Range", method = RequestMethod.GET)
public void handleRequest(@PathVariable(value = "fileName") String fileName, HttpServletRequest request, HttpServletResponse response) throws Exception {

    File file = fileRepository.getFileByName(fileName)
    response.addHeader("Accept-Ranges", "bytes");
    response.addDateHeader("Last-Modified", file.lastModified());

    Long fileSize = file.length();
    response.addHeader(HttpHeaderConstants.CONTENT_LENGTH, fileSize.toString());

    // Stream file to end user client.
    fileDownloadHandler.handle(request, response, file);
}
1
You can add HttpServletResponse response as a parameter to your method annotated with @GetMapping and do something like response.setHeader("Content-Length", "12345");Impulse The Fox
@ImpulseTheFox That is what we are trying to do. I have added a code that use to work prior to the changes in Spring 4.3. The handleHeadRequest method is no longer called at all. :(hooknc
Your question is how to make download service controller?0gam
@Byeon0gam, no, not really. I am asking how can I get Spring to call the handleHeadRequest method above instead of the handleRequest method for HTTP HEAD method requests.hooknc
@hooknc, you checked my post?0gam

1 Answers

0
votes

The HTTP HEAD method requests the headers that are returned if the specified resource would be requested with an HTTP GET method. Such a request can be done before deciding to download a large resource to save bandwidth, for example.

A response to a HEAD method should not have a body. If so, it must be ignored. Even so, entity headers describing the content of the body, like Content-Length may be included in the response. They don't relate to the body of the HEAD response, which should be empty, but to the body of similar request using the GET method would have returned as a response.

If the result of a HEAD request shows that a cached resource after a GET request is now outdated, the cache is invalidated, even if no GET request has been made.

  • Request has body No
  • Successful response has body No
  • Safe Yes
  • Idempotent Yes
  • Cacheable Yes
  • Allowed in HTML forms No

Implicit HEAD support From Spring's MVC doc:

@RequestMapping methods mapped to "GET" are also implicitly mapped to "HEAD", i.e. there is no need to have "HEAD" explicitly declared. An HTTP HEAD request is processed as if it were an HTTP GET except instead of writing the body only the number of bytes are counted and the "Content-Length" header set.

Check point : That means we never have to separately create a handler method for HTTP HEAD verb as spring implicitly supports that, given that GET verb is already defined for the target URL.


Example

The Controller

Let's create a very simple controller with a handler method populating some headers:

@Controller
public class MyController {
    Logger logger = Logger.getLogger(MyController.class.getSimpleName());

    @RequestMapping(value = "test", method = {RequestMethod.GET})
    public HttpEntity<String> handleTestRequest () {

        MultiValueMap<String, String> headers = new HttpHeaders();
        headers.put("test-header", Arrays.asList("test-header-value"));

        HttpEntity<String> responseEntity = new HttpEntity<>("test body", headers);


        logger.info("handler finished");
        return responseEntity;
    }
} 

JUnit Tests

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes = MyWebConfig.class)
public class ControllerTest {

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @Before
    public void setup () {
        DefaultMockMvcBuilder builder = MockMvcBuilders.webAppContextSetup(this.wac);
        this.mockMvc = builder.build();
    }

    @Test
    public void testGet () throws Exception {

        MockHttpServletRequestBuilder builder =
                            MockMvcRequestBuilders.get("/test");

        this.mockMvc.perform(builder)
                    .andExpect(MockMvcResultMatchers.status()
                                                    .isOk())
                    .andDo(MockMvcResultHandlers.print());
    }

    @Test
    public void testHead () throws Exception {

        MockHttpServletRequestBuilder builder =
                            MockMvcRequestBuilders.head("/test");

        this.mockMvc.perform(builder)
                    .andExpect(MockMvcResultMatchers.status()
                                                    .isOk())
                    .andDo(MockMvcResultHandlers.print());
    }
}

Implicit OPTIONS support From Spring's MVC doc:

@RequestMapping methods have built-in support for HTTP OPTIONS. By default an HTTP OPTIONS request is handled by setting the "Allow" response header to the HTTP methods explicitly declared on all @RequestMapping methods with matching URL patterns. When no HTTP methods are explicitly declared the "Allow" header is set to "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS"

Check point : That means, we never have to separately create a handler method for HTTP OPTIONS verb as spring implicitly supports that, given that all handler methods explicitly specify the HTTP method with each @RequestMapping for the target URL.


Example

Let's continue with the above example just add one more test for the HTTP OPTIONS verb:

@Test
public void testOptions () throws Exception {

    ResultMatcher accessHeader = MockMvcResultMatchers.header()
                                                      .string("Allow", "GET,HEAD");

    MockHttpServletRequestBuilder builder =
                        MockMvcRequestBuilders.options("/test");

    this.mockMvc.perform(builder)
                .andExpect(MockMvcResultMatchers.status()
                                                .isOk())
                .andExpect(accessHeader)
                .andDo(MockMvcResultHandlers.print());
}