diff --git a/spring-web/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java b/spring-web/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java index 1e1f7d9d05d..67c2b63adc0 100644 --- a/spring-web/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java +++ b/spring-web/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2021 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. @@ -20,9 +20,12 @@ import java.util.Collections; import java.util.List; import org.springframework.core.ResolvableType; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; /** * Exception for errors that fit response status 415 (unsupported media type). @@ -41,6 +44,9 @@ public class UnsupportedMediaTypeStatusException extends ResponseStatusException @Nullable private final ResolvableType bodyType; + @Nullable + private final HttpMethod method; + /** * Constructor for when the specified Content-Type is invalid. @@ -50,13 +56,14 @@ public class UnsupportedMediaTypeStatusException extends ResponseStatusException this.contentType = null; this.supportedMediaTypes = Collections.emptyList(); this.bodyType = null; + this.method = null; } /** * Constructor for when the Content-Type can be parsed but is not supported. */ public UnsupportedMediaTypeStatusException(@Nullable MediaType contentType, List supportedTypes) { - this(contentType, supportedTypes, null); + this(contentType, supportedTypes, null, null); } /** @@ -65,11 +72,30 @@ public class UnsupportedMediaTypeStatusException extends ResponseStatusException */ public UnsupportedMediaTypeStatusException(@Nullable MediaType contentType, List supportedTypes, @Nullable ResolvableType bodyType) { + this(contentType, supportedTypes, bodyType, null); + } + + /** + * Constructor that provides the HTTP method. + * @since 5.3.6 + */ + public UnsupportedMediaTypeStatusException(@Nullable MediaType contentType, List supportedTypes, + @Nullable HttpMethod method) { + this(contentType, supportedTypes, null, method); + } + + /** + * Constructor for when trying to encode from or decode to a specific Java type. + * @since 5.3.6 + */ + public UnsupportedMediaTypeStatusException(@Nullable MediaType contentType, List supportedTypes, + @Nullable ResolvableType bodyType, @Nullable HttpMethod method) { super(HttpStatus.UNSUPPORTED_MEDIA_TYPE, initReason(contentType, bodyType)); this.contentType = contentType; this.supportedMediaTypes = Collections.unmodifiableList(supportedTypes); this.bodyType = bodyType; + this.method = method; } private static String initReason(@Nullable MediaType contentType, @Nullable ResolvableType bodyType) { @@ -107,4 +133,14 @@ public class UnsupportedMediaTypeStatusException extends ResponseStatusException return this.bodyType; } + @Override + public HttpHeaders getResponseHeaders() { + if (HttpMethod.PATCH != this.method || CollectionUtils.isEmpty(this.supportedMediaTypes) ) { + return HttpHeaders.EMPTY; + } + HttpHeaders headers = new HttpHeaders(); + headers.setAcceptPatch(this.supportedMediaTypes); + return headers; + } + } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java index 23fe152e860..fe4478e646e 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java @@ -190,7 +190,7 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe catch (InvalidMediaTypeException ex) { throw new UnsupportedMediaTypeStatusException(ex.getMessage()); } - throw new UnsupportedMediaTypeStatusException(contentType, new ArrayList<>(mediaTypes)); + throw new UnsupportedMediaTypeStatusException(contentType, new ArrayList<>(mediaTypes), exchange.getRequest().getMethod()); } if (helper.hasProducesMismatch()) { diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java index 79b6813bde6..cbc923abe1b 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java @@ -317,6 +317,26 @@ public class RequestMappingInfoHandlerMappingTests { assertThat(uriVariables.get("cars")).isEqualTo("cars"); } + @Test + public void handlePatchUnsupportedMediaType() { + MockServerHttpRequest request = MockServerHttpRequest.patch("/qux") + .header("content-type", "application/xml") + .build(); + ServerWebExchange exchange = MockServerWebExchange.from(request); + Mono mono = this.handlerMapping.getHandler(exchange); + + StepVerifier.create(mono) + .expectErrorSatisfies(ex -> { + assertThat(ex).isInstanceOf(UnsupportedMediaTypeStatusException.class); + UnsupportedMediaTypeStatusException umtse = (UnsupportedMediaTypeStatusException) ex; + MediaType mediaType = new MediaType("foo", "bar"); + assertThat(umtse.getSupportedMediaTypes()).containsExactly(mediaType); + assertThat(umtse.getResponseHeaders().getAcceptPatch()).containsExactly(mediaType); + }) + .verify(); + + } + @SuppressWarnings("unchecked") private void assertError(Mono mono, final Class exceptionClass, final Consumer consumer) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java index 5db290d6072..f706afe2d4c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 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. @@ -231,6 +231,12 @@ public abstract class ResponseEntityExceptionHandler { List mediaTypes = ex.getSupportedMediaTypes(); if (!CollectionUtils.isEmpty(mediaTypes)) { headers.setAccept(mediaTypes); + if (request instanceof ServletWebRequest) { + ServletWebRequest servletWebRequest = (ServletWebRequest) request; + if (HttpMethod.PATCH.equals(servletWebRequest.getHttpMethod())) { + headers.setAcceptPatch(mediaTypes); + } + } } return handleExceptionInternal(ex, null, headers, status, request); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java index a5f1d79adc6..542f0ae8f87 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java @@ -281,6 +281,9 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes List mediaTypes = ex.getSupportedMediaTypes(); if (!CollectionUtils.isEmpty(mediaTypes)) { response.setHeader("Accept", MediaType.toString(mediaTypes)); + if (request.getMethod().equals("PATCH")) { + response.setHeader("Accept-Patch", MediaType.toString(mediaTypes)); + } } response.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE); return new ModelAndView(); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java index 6cc605353cc..e5fa5c68473 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 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. @@ -113,6 +113,20 @@ public class ResponseEntityExceptionHandlerTests { ResponseEntity responseEntity = testException(ex); assertThat(responseEntity.getHeaders().getAccept()).isEqualTo(acceptable); + assertThat(responseEntity.getHeaders().getAcceptPatch()).isEmpty(); + } + + @Test + public void patchHttpMediaTypeNotSupported() { + this.servletRequest = new MockHttpServletRequest("PATCH", "/"); + this.request = new ServletWebRequest(this.servletRequest, this.servletResponse); + + List acceptable = Arrays.asList(MediaType.APPLICATION_ATOM_XML, MediaType.APPLICATION_XML); + Exception ex = new HttpMediaTypeNotSupportedException(MediaType.APPLICATION_JSON, acceptable); + + ResponseEntity responseEntity = testException(ex); + assertThat(responseEntity.getHeaders().getAccept()).isEqualTo(acceptable); + assertThat(responseEntity.getHeaders().getAcceptPatch()).isEqualTo(acceptable); } @Test 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 e6e6ceb3ca8..eba67ab75e4 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -976,6 +976,26 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getHeader("Accept")).isEqualTo("text/plain"); } + @PathPatternsParameterizedTest + void unsupportedPatchBody(boolean usePathPatterns) throws Exception { + initDispatcherServlet(RequestResponseBodyController.class, usePathPatterns, wac -> { + RootBeanDefinition adapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class); + StringHttpMessageConverter converter = new StringHttpMessageConverter(); + converter.setSupportedMediaTypes(Collections.singletonList(MediaType.TEXT_PLAIN)); + adapterDef.getPropertyValues().add("messageConverters", converter); + wac.registerBeanDefinition("handlerAdapter", adapterDef); + }); + + MockHttpServletRequest request = new MockHttpServletRequest("PATCH", "/something"); + String requestBody = "Hello World"; + request.setContent(requestBody.getBytes(StandardCharsets.UTF_8)); + request.addHeader("Content-Type", "application/pdf"); + MockHttpServletResponse response = new MockHttpServletResponse(); + getServlet().service(request, response); + assertThat(response.getStatus()).isEqualTo(415); + assertThat(response.getHeader("Accept-Patch")).isEqualTo("text/plain"); + } + @PathPatternsParameterizedTest void responseBodyNoAcceptHeader(boolean usePathPatterns) throws Exception { initDispatcherServlet(RequestResponseBodyController.class, usePathPatterns); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolverTests.java index 4eecb9d575a..e2574d4224c 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolverTests.java @@ -87,6 +87,18 @@ public class DefaultHandlerExceptionResolverTests { assertThat(response.getHeader("Accept")).as("Invalid Accept header").isEqualTo("application/pdf"); } + @Test + public void patchHttpMediaTypeNotSupported() { + HttpMediaTypeNotSupportedException ex = new HttpMediaTypeNotSupportedException(new MediaType("text", "plain"), + Collections.singletonList(new MediaType("application", "pdf"))); + MockHttpServletRequest request = new MockHttpServletRequest("PATCH", "/"); + ModelAndView mav = exceptionResolver.resolveException(request, response, null, ex); + assertThat(mav).as("No ModelAndView returned").isNotNull(); + assertThat(mav.isEmpty()).as("No Empty ModelAndView returned").isTrue(); + assertThat(response.getStatus()).as("Invalid status code").isEqualTo(415); + assertThat(response.getHeader("Accept-Patch")).as("Invalid Accept header").isEqualTo("application/pdf"); + } + @Test public void handleMissingPathVariable() throws NoSuchMethodException { Method method = getClass().getMethod("handle", String.class);