From d70ad765bf17efdf8dc55484aaf224ec0dea440c Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Sun, 24 Jan 2016 19:35:45 -0500 Subject: [PATCH] Support HTTP HEAD Issue: SPR-13130 --- .../web/cors/CorsConfiguration.java | 3 +- .../web/filter/ShallowEtagHeaderFilter.java | 7 ++- .../web/cors/CorsConfigurationTests.java | 11 ++-- .../web/cors/DefaultCorsProcessorTests.java | 4 +- .../RequestMethodsRequestCondition.java | 19 +++++-- .../annotation/HttpEntityMethodProcessor.java | 6 +- .../RequestMethodsRequestConditionTests.java | 20 +++++++ ...nnotationControllerHandlerMethodTests.java | 55 ++++++++++++++----- 8 files changed, 95 insertions(+), 30 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java index 76daa33bbc8..016a355be49 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java +++ b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -340,6 +340,7 @@ public class CorsConfiguration { } if (allowedMethods.isEmpty()) { allowedMethods.add(HttpMethod.GET.name()); + allowedMethods.add(HttpMethod.HEAD.name()); } List result = new ArrayList(allowedMethods.size()); boolean allowed = false; diff --git a/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java b/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java index b3fbe838266..c5a89c8a742 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -144,7 +144,10 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter { protected boolean isEligibleForEtag(HttpServletRequest request, HttpServletResponse response, int responseStatusCode, InputStream inputStream) { - if (responseStatusCode >= 200 && responseStatusCode < 300 && HttpMethod.GET.matches(request.getMethod())) { + String method = request.getMethod(); + if (responseStatusCode >= 200 && responseStatusCode < 300 && + (HttpMethod.GET.matches(method) || HttpMethod.HEAD.matches(method))) { + String cacheControl = null; if (servlet3Present) { cacheControl = response.getHeader(HEADER_CACHE_CONTROL); diff --git a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java index 8d3651c5006..f777f6c6e29 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,10 @@ import org.junit.Test; import org.springframework.http.HttpMethod; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; /** * Unit tests for {@link CorsConfiguration}. @@ -176,7 +179,7 @@ public class CorsConfigurationTests { @Test public void checkMethodAllowed() { - assertEquals(Arrays.asList(HttpMethod.GET), config.checkHttpMethod(HttpMethod.GET)); + assertEquals(Arrays.asList(HttpMethod.GET, HttpMethod.HEAD), config.checkHttpMethod(HttpMethod.GET)); config.addAllowedMethod("GET"); assertEquals(Arrays.asList(HttpMethod.GET), config.checkHttpMethod(HttpMethod.GET)); config.addAllowedMethod("POST"); @@ -189,7 +192,7 @@ public class CorsConfigurationTests { assertNull(config.checkHttpMethod(null)); assertNull(config.checkHttpMethod(HttpMethod.DELETE)); config.setAllowedMethods(new ArrayList<>()); - assertNull(config.checkHttpMethod(HttpMethod.HEAD)); + assertNull(config.checkHttpMethod(HttpMethod.POST)); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java index 56ab6166f4b..30a93e30856 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -171,7 +171,7 @@ public class DefaultCorsProcessorTests { this.conf.addAllowedOrigin("*"); this.processor.processRequest(this.conf, request, response); assertEquals(HttpServletResponse.SC_OK, response.getStatus()); - assertEquals("GET", response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)); + assertEquals("GET,HEAD", response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)); } @Test diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/RequestMethodsRequestCondition.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/RequestMethodsRequestCondition.java index 6d18fcf2e40..d73d1ab1cf7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/RequestMethodsRequestCondition.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/RequestMethodsRequestCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,10 @@ import org.springframework.web.bind.annotation.RequestMethod; */ public final class RequestMethodsRequestCondition extends AbstractRequestCondition { + private static final RequestMethodsRequestCondition HEAD_CONDITION = + new RequestMethodsRequestCondition(RequestMethod.HEAD); + + private final Set methods; @@ -98,17 +102,24 @@ public final class RequestMethodsRequestCondition extends AbstractRequestConditi if (this.methods.isEmpty()) { return this; } - RequestMethod incomingRequestMethod = getRequestMethod(request); - if (incomingRequestMethod != null) { + RequestMethod requestMethod = getRequestMethod(request); + if (requestMethod != null) { for (RequestMethod method : this.methods) { - if (method.equals(incomingRequestMethod)) { + if (method.equals(requestMethod)) { return new RequestMethodsRequestCondition(method); } } + if (isHeadRequest(requestMethod) && getMethods().contains(RequestMethod.GET)) { + return HEAD_CONDITION; + } } return null; } + private boolean isHeadRequest(RequestMethod requestMethod) { + return (requestMethod != null && RequestMethod.HEAD.equals(requestMethod)); + } + private RequestMethod getRequestMethod(HttpServletRequest request) { try { return RequestMethod.valueOf(request.getMethod()); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java index 7741b1c4db4..f69a1c2e3e6 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -170,7 +170,9 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro Object body = responseEntity.getBody(); if (responseEntity instanceof ResponseEntity) { outputMessage.setStatusCode(((ResponseEntity) responseEntity).getStatusCode()); - if (HttpMethod.GET == inputMessage.getMethod() && isResourceNotModified(inputMessage, outputMessage)) { + HttpMethod method = inputMessage.getMethod(); + boolean isGetOrHead = (HttpMethod.GET == method || HttpMethod.HEAD == method); + if (isGetOrHead && isResourceNotModified(inputMessage, outputMessage)) { outputMessage.setStatusCode(HttpStatus.NOT_MODIFIED); // Ensure headers are flushed, no body should be written. outputMessage.flush(); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/RequestMethodsRequestConditionTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/RequestMethodsRequestConditionTests.java index 4d147391662..94dc781511e 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/RequestMethodsRequestConditionTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/RequestMethodsRequestConditionTests.java @@ -32,6 +32,7 @@ import static org.springframework.web.bind.annotation.RequestMethod.POST; /** * @author Arjen Poutsma + * @author Rossen Stoyanchev */ public class RequestMethodsRequestConditionTests { @@ -61,6 +62,25 @@ public class RequestMethodsRequestConditionTests { assertEquals(Collections.singleton(GET), actual.getContent()); } + @Test + public void methodHeadMatch() throws Exception { + RequestMethodsRequestCondition condition = new RequestMethodsRequestCondition(GET, POST); + MockHttpServletRequest request = new MockHttpServletRequest("HEAD", "/foo"); + RequestMethodsRequestCondition actual = condition.getMatchingCondition(request); + + assertNotNull(actual); + assertEquals("GET should also match HEAD", Collections.singleton(HEAD), actual.getContent()); + } + + @Test + public void methodHeadNoMatch() throws Exception { + RequestMethodsRequestCondition condition = new RequestMethodsRequestCondition(POST); + MockHttpServletRequest request = new MockHttpServletRequest("HEAD", "/foo"); + RequestMethodsRequestCondition actual = condition.getMatchingCondition(request); + + assertNull("HEAD should match only if GET is declared", actual); + } + @Test public void noDeclaredMethodsMatchesAllMethods() { RequestCondition condition = new RequestMethodsRequestCondition(); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java index f60ae38b588..a93b70ab39c 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java @@ -19,7 +19,6 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.beans.PropertyEditorSupport; import java.io.IOException; import java.io.Serializable; -import java.io.UnsupportedEncodingException; import java.io.Writer; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -968,7 +967,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl public void httpEntity() throws ServletException, IOException { initServletWithControllers(ResponseEntityController.class); - MockHttpServletRequest request = new MockHttpServletRequest("PUT", "/foo"); + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/foo"); String requestBody = "Hello World"; request.setContent(requestBody.getBytes("UTF-8")); request.addHeader("Content-Type", "text/plain; charset=utf-8"); @@ -980,7 +979,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertEquals(requestBody, response.getContentAsString()); assertEquals("MyValue", response.getHeader("MyResponseHeader")); - request = new MockHttpServletRequest("PUT", "/bar"); + request = new MockHttpServletRequest("GET", "/bar"); response = new MockHttpServletResponse(); getServlet().service(request, response); assertEquals("MyValue", response.getHeader("MyResponseHeader")); @@ -1748,6 +1747,30 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertEquals("view", response.getForwardedUrl()); } + @Test + public void httpHead() throws ServletException, IOException { + initServletWithControllers(ResponseEntityController.class); + + MockHttpServletRequest request = new MockHttpServletRequest("HEAD", "/baz"); + MockHttpServletResponse response = new MockHttpServletResponse(); + getServlet().service(request, response); + + assertEquals(200, response.getStatus()); + assertEquals("MyValue", response.getHeader("MyResponseHeader")); + assertEquals(4, response.getContentLength()); + assertTrue(response.getContentAsByteArray().length == 0); + + // Now repeat with GET + request = new MockHttpServletRequest("GET", "/baz"); + response = new MockHttpServletResponse(); + getServlet().service(request, response); + + assertEquals(200, response.getStatus()); + assertEquals("MyValue", response.getHeader("MyResponseHeader")); + assertEquals(4, response.getContentLength()); + assertEquals("body", response.getContentAsString()); + } + /* * Controllers @@ -3019,25 +3042,27 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl @Controller public static class ResponseEntityController { - @RequestMapping("/foo") - public ResponseEntity foo(HttpEntity requestEntity) throws UnsupportedEncodingException { + @RequestMapping(path = "/foo", method = RequestMethod.POST) + public ResponseEntity foo(HttpEntity requestEntity) throws Exception { assertNotNull(requestEntity); assertEquals("MyValue", requestEntity.getHeaders().getFirst("MyRequestHeader")); - String requestBody = new String(requestEntity.getBody(), "UTF-8"); - assertEquals("Hello World", requestBody); - HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.set("MyResponseHeader", "MyValue"); - return new ResponseEntity(requestBody, responseHeaders, HttpStatus.CREATED); + String body = new String(requestEntity.getBody(), "UTF-8"); + assertEquals("Hello World", body); + + URI location = new URI("/foo"); + return ResponseEntity.created(location).header("MyResponseHeader", "MyValue").body(body); } - @RequestMapping("/bar") - public ResponseEntity bar() { - HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.set("MyResponseHeader", "MyValue"); - return new ResponseEntity(responseHeaders, HttpStatus.NOT_FOUND); + @RequestMapping(path = "/bar", method = RequestMethod.GET) + public ResponseEntity bar() { + return ResponseEntity.notFound().header("MyResponseHeader", "MyValue").build(); } + @RequestMapping("/baz") + public ResponseEntity baz() { + return ResponseEntity.ok().header("MyResponseHeader", "MyValue").body("body"); + } } @Controller