Support content negotiation for RFC 7807
Closes gh-28189
This commit is contained in:
@@ -19,8 +19,10 @@ package org.springframework.web.servlet.mvc.method.annotation;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
@@ -44,6 +46,7 @@ import org.springframework.http.HttpRange;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.MediaTypeFactory;
|
||||
import org.springframework.http.ProblemDetail;
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||
@@ -93,6 +96,9 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
|
||||
|
||||
private final ContentNegotiationManager contentNegotiationManager;
|
||||
|
||||
private final List<MediaType> problemMediaTypes =
|
||||
Arrays.asList(MediaType.APPLICATION_PROBLEM_JSON, MediaType.APPLICATION_PROBLEM_XML);
|
||||
|
||||
private final Set<String> safeExtensions = new HashSet<>();
|
||||
|
||||
|
||||
@@ -227,21 +233,22 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
|
||||
|
||||
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
|
||||
if (body != null && producibleTypes.isEmpty()) {
|
||||
throw new HttpMessageNotWritableException(
|
||||
"No converter found for return value of type: " + valueType);
|
||||
}
|
||||
List<MediaType> mediaTypesToUse = new ArrayList<>();
|
||||
for (MediaType requestedType : acceptableTypes) {
|
||||
for (MediaType producibleType : producibleTypes) {
|
||||
if (requestedType.isCompatibleWith(producibleType)) {
|
||||
mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
|
||||
}
|
||||
}
|
||||
|
||||
List<MediaType> compatibleMediaTypes = new ArrayList<>();
|
||||
determineCompatibleMediaTypes(acceptableTypes, producibleTypes, compatibleMediaTypes);
|
||||
|
||||
// Fall back on RFC 7807 format for ProblemDetail
|
||||
if (compatibleMediaTypes.isEmpty() && ProblemDetail.class.isAssignableFrom(valueType)) {
|
||||
determineCompatibleMediaTypes(this.problemMediaTypes, producibleTypes, compatibleMediaTypes);
|
||||
}
|
||||
if (mediaTypesToUse.isEmpty()) {
|
||||
|
||||
if (compatibleMediaTypes.isEmpty()) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes);
|
||||
}
|
||||
@@ -251,9 +258,9 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
|
||||
return;
|
||||
}
|
||||
|
||||
MimeTypeUtils.sortBySpecificity(mediaTypesToUse);
|
||||
MimeTypeUtils.sortBySpecificity(compatibleMediaTypes);
|
||||
|
||||
for (MediaType mediaType : mediaTypesToUse) {
|
||||
for (MediaType mediaType : compatibleMediaTypes) {
|
||||
if (mediaType.isConcrete()) {
|
||||
selectedMediaType = mediaType;
|
||||
break;
|
||||
@@ -374,7 +381,7 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
|
||||
if (!CollectionUtils.isEmpty(mediaTypes)) {
|
||||
return new ArrayList<>(mediaTypes);
|
||||
}
|
||||
List<MediaType> result = new ArrayList<>();
|
||||
Set<MediaType> result = new LinkedHashSet<>();
|
||||
for (HttpMessageConverter<?> converter : this.messageConverters) {
|
||||
if (converter instanceof GenericHttpMessageConverter && targetType != null) {
|
||||
if (((GenericHttpMessageConverter<?>) converter).canWrite(targetType, valueClass, null)) {
|
||||
@@ -385,7 +392,7 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
|
||||
result.addAll(converter.getSupportedMediaTypes(valueClass));
|
||||
}
|
||||
}
|
||||
return (result.isEmpty() ? Collections.singletonList(MediaType.ALL) : result);
|
||||
return (result.isEmpty() ? Collections.singletonList(MediaType.ALL) : new ArrayList<>(result));
|
||||
}
|
||||
|
||||
private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request)
|
||||
@@ -394,6 +401,18 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
|
||||
return this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
|
||||
}
|
||||
|
||||
private void determineCompatibleMediaTypes(
|
||||
List<MediaType> acceptableTypes, List<MediaType> producibleTypes, List<MediaType> mediaTypesToUse) {
|
||||
|
||||
for (MediaType requestedType : acceptableTypes) {
|
||||
for (MediaType producibleType : producibleTypes) {
|
||||
if (requestedType.isCompatibleWith(producibleType)) {
|
||||
mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the more specific of the acceptable and the producible media types
|
||||
* with the q-value of the former.
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.web.servlet.mvc.method.annotation;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -25,6 +26,8 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.core.Conventions;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.ProblemDetail;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||
@@ -179,6 +182,14 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter
|
||||
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
|
||||
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
|
||||
|
||||
if (returnValue instanceof ProblemDetail detail) {
|
||||
outputMessage.setStatusCode(HttpStatusCode.valueOf(detail.getStatus()));
|
||||
if (detail.getInstance() == null) {
|
||||
URI path = URI.create(inputMessage.getServletRequest().getRequestURI());
|
||||
detail.setInstance(path);
|
||||
}
|
||||
}
|
||||
|
||||
// Try even with null return value. ResponseBodyAdvice could get involved.
|
||||
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -43,6 +43,7 @@ import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ProblemDetail;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
@@ -393,6 +394,48 @@ public class RequestResponseBodyMethodProcessorTests {
|
||||
"}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void problemDetailDefaultMediaType() throws Exception {
|
||||
testProblemDetailMediaType(MediaType.APPLICATION_PROBLEM_JSON_VALUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void problemDetailWhenJsonRequested() throws Exception {
|
||||
this.servletRequest.addHeader("Accept", MediaType.APPLICATION_JSON_VALUE);
|
||||
testProblemDetailMediaType(MediaType.APPLICATION_JSON_VALUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void problemDetailWhenNoMatchingMediaTypeRequested() throws Exception {
|
||||
this.servletRequest.addHeader("Accept", MediaType.APPLICATION_PDF_VALUE);
|
||||
testProblemDetailMediaType(MediaType.APPLICATION_PROBLEM_JSON_VALUE);
|
||||
}
|
||||
|
||||
private void testProblemDetailMediaType(String expectedContentType) throws Exception {
|
||||
|
||||
ProblemDetail problemDetail = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
|
||||
|
||||
this.servletRequest.setRequestURI("/path");
|
||||
|
||||
RequestResponseBodyMethodProcessor processor =
|
||||
new RequestResponseBodyMethodProcessor(
|
||||
Collections.singletonList(new MappingJackson2HttpMessageConverter()));
|
||||
|
||||
MethodParameter returnType =
|
||||
new MethodParameter(getClass().getDeclaredMethod("handleAndReturnProblemDetail"), -1);
|
||||
|
||||
processor.handleReturnValue(problemDetail, returnType, this.container, this.request);
|
||||
|
||||
assertThat(this.servletResponse.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||
assertThat(this.servletResponse.getContentType()).isEqualTo(expectedContentType);
|
||||
assertThat(this.servletResponse.getContentAsString()).isEqualTo(
|
||||
"{\"type\":\"about:blank\"," +
|
||||
"\"title\":\"Bad Request\"," +
|
||||
"\"status\":400," +
|
||||
"\"detail\":null," +
|
||||
"\"instance\":\"/path\"}");
|
||||
}
|
||||
|
||||
@Test // SPR-13135
|
||||
public void handleReturnValueWithInvalidReturnType() throws Exception {
|
||||
Method method = getClass().getDeclaredMethod("handleAndReturnOutputStream");
|
||||
@@ -806,6 +849,10 @@ public class RequestResponseBodyMethodProcessorTests {
|
||||
return null;
|
||||
}
|
||||
|
||||
ProblemDetail handleAndReturnProblemDetail() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@RequestMapping
|
||||
OutputStream handleAndReturnOutputStream() {
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user