Skip to content

Commit d8441fc

Browse files
committed
Invoke handleEmptyBody if there is no content-type
Closes gh-30522
1 parent d8ed7c7 commit d8441fc

File tree

2 files changed

+104
-17
lines changed

2 files changed

+104
-17
lines changed

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.lang.reflect.Type;
2424
import java.util.ArrayList;
2525
import java.util.Collection;
26+
import java.util.Collections;
2627
import java.util.LinkedHashSet;
2728
import java.util.List;
2829
import java.util.Optional;
@@ -38,6 +39,7 @@
3839
import org.springframework.http.HttpHeaders;
3940
import org.springframework.http.HttpInputMessage;
4041
import org.springframework.http.HttpMethod;
42+
import org.springframework.http.HttpOutputMessage;
4143
import org.springframework.http.HttpRequest;
4244
import org.springframework.http.InvalidMediaTypeException;
4345
import org.springframework.http.MediaType;
@@ -170,7 +172,6 @@ protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, Me
170172
EmptyBodyCheckingHttpInputMessage message = null;
171173
try {
172174
message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
173-
174175
for (HttpMessageConverter<?> converter : this.messageConverters) {
175176
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
176177
GenericHttpMessageConverter<?> genericConverter =
@@ -190,6 +191,10 @@ protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, Me
190191
break;
191192
}
192193
}
194+
if (body == NO_VALUE && noContentType && !message.hasBody()) {
195+
body = getAdvice().handleEmptyBody(
196+
null, message, parameter, targetType, NoContentTypeHttpMessageConverter.class);
197+
}
193198
}
194199
catch (IOException ex) {
195200
throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
@@ -201,8 +206,7 @@ protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, Me
201206
}
202207

203208
if (body == NO_VALUE) {
204-
if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||
205-
(noContentType && !message.hasBody())) {
209+
if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) || (noContentType && !message.hasBody())) {
206210
return null;
207211
}
208212
throw new HttpMediaTypeNotSupportedException(contentType,
@@ -354,4 +358,38 @@ public boolean hasBody() {
354358
}
355359
}
356360

361+
362+
/**
363+
* Placeholder HttpMessageConverter type to pass to RequestBodyAdvice if there
364+
* is no content-type and no content. In that case, we may not find a converter,
365+
* but RequestBodyAdvice have a chance to provide it via handleEmptyBody.
366+
*/
367+
private static class NoContentTypeHttpMessageConverter implements HttpMessageConverter<String> {
368+
369+
@Override
370+
public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
371+
return false;
372+
}
373+
374+
@Override
375+
public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
376+
return false;
377+
}
378+
379+
@Override
380+
public List<MediaType> getSupportedMediaTypes() {
381+
return Collections.emptyList();
382+
}
383+
384+
@Override
385+
public String read(Class<? extends String> clazz, HttpInputMessage inputMessage) {
386+
throw new UnsupportedOperationException();
387+
}
388+
389+
@Override
390+
public void write(String s, @Nullable MediaType contentType, HttpOutputMessage outputMessage) {
391+
throw new UnsupportedOperationException();
392+
}
393+
}
394+
357395
}

spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.List;
2626
import java.util.Map;
2727

28+
import org.apache.groovy.util.Maps;
2829
import org.junit.jupiter.api.BeforeAll;
2930
import org.junit.jupiter.api.BeforeEach;
3031
import org.junit.jupiter.api.Test;
@@ -45,6 +46,8 @@
4546
import org.springframework.ui.Model;
4647
import org.springframework.web.bind.annotation.ControllerAdvice;
4748
import org.springframework.web.bind.annotation.ModelAttribute;
49+
import org.springframework.web.bind.annotation.RequestBody;
50+
import org.springframework.web.bind.annotation.ResponseBody;
4851
import org.springframework.web.bind.annotation.SessionAttributes;
4952
import org.springframework.web.context.support.StaticWebApplicationContext;
5053
import org.springframework.web.method.HandlerMethod;
@@ -109,7 +112,7 @@ public void setup() throws Exception {
109112

110113
@Test
111114
public void cacheControlWithoutSessionAttributes() throws Exception {
112-
HandlerMethod handlerMethod = handlerMethod(new SimpleController(), "handle");
115+
HandlerMethod handlerMethod = handlerMethod(new TestController(), "handle");
113116
this.handlerAdapter.setCacheSeconds(100);
114117
this.handlerAdapter.afterPropertiesSet();
115118

@@ -197,7 +200,7 @@ public void modelAttributeAdvice() throws Exception {
197200
this.webAppContext.registerSingleton("maa", ModelAttributeAdvice.class);
198201
this.webAppContext.refresh();
199202

200-
HandlerMethod handlerMethod = handlerMethod(new SimpleController(), "handle");
203+
HandlerMethod handlerMethod = handlerMethod(new TestController(), "handle");
201204
this.handlerAdapter.afterPropertiesSet();
202205
ModelAndView mav = this.handlerAdapter.handle(this.request, this.response, handlerMethod);
203206

@@ -210,7 +213,7 @@ public void prototypeControllerAdvice() throws Exception {
210213
this.webAppContext.registerPrototype("maa", ModelAttributeAdvice.class);
211214
this.webAppContext.refresh();
212215

213-
HandlerMethod handlerMethod = handlerMethod(new SimpleController(), "handle");
216+
HandlerMethod handlerMethod = handlerMethod(new TestController(), "handle");
214217
this.handlerAdapter.afterPropertiesSet();
215218
Map<String, Object> model1 = this.handlerAdapter.handle(this.request, this.response, handlerMethod).getModel();
216219
Map<String, Object> model2 = this.handlerAdapter.handle(this.request, this.response, handlerMethod).getModel();
@@ -226,7 +229,7 @@ public void modelAttributeAdviceInParentContext() throws Exception {
226229
this.webAppContext.setParent(parent);
227230
this.webAppContext.refresh();
228231

229-
HandlerMethod handlerMethod = handlerMethod(new SimpleController(), "handle");
232+
HandlerMethod handlerMethod = handlerMethod(new TestController(), "handle");
230233
this.handlerAdapter.afterPropertiesSet();
231234
ModelAndView mav = this.handlerAdapter.handle(this.request, this.response, handlerMethod);
232235

@@ -240,7 +243,7 @@ public void modelAttributePackageNameAdvice() throws Exception {
240243
this.webAppContext.registerSingleton("manupa", ModelAttributeNotUsedPackageAdvice.class);
241244
this.webAppContext.refresh();
242245

243-
HandlerMethod handlerMethod = handlerMethod(new SimpleController(), "handle");
246+
HandlerMethod handlerMethod = handlerMethod(new TestController(), "handle");
244247
this.handlerAdapter.afterPropertiesSet();
245248
ModelAndView mav = this.handlerAdapter.handle(this.request, this.response, handlerMethod);
246249

@@ -249,9 +252,7 @@ public void modelAttributePackageNameAdvice() throws Exception {
249252
assertThat(mav.getModel().get("attr3")).isNull();
250253
}
251254

252-
// SPR-10859
253-
254-
@Test
255+
@Test // gh-15486
255256
public void responseBodyAdvice() throws Exception {
256257
List<HttpMessageConverter<?>> converters = new ArrayList<>();
257258
converters.add(new MappingJackson2HttpMessageConverter());
@@ -263,14 +264,27 @@ public void responseBodyAdvice() throws Exception {
263264
this.request.addHeader("Accept", MediaType.APPLICATION_JSON_VALUE);
264265
this.request.setParameter("c", "callback");
265266

266-
HandlerMethod handlerMethod = handlerMethod(new SimpleController(), "handleBadRequest");
267+
HandlerMethod handlerMethod = handlerMethod(new TestController(), "handleBadRequest");
267268
this.handlerAdapter.afterPropertiesSet();
268269
this.handlerAdapter.handle(this.request, this.response, handlerMethod);
269270

270271
assertThat(this.response.getStatus()).isEqualTo(200);
271272
assertThat(this.response.getContentAsString()).isEqualTo("{\"status\":400,\"message\":\"body\"}");
272273
}
273274

275+
@Test // gh-30522
276+
public void responseBodyAdviceWithEmptyBody() throws Exception {
277+
this.webAppContext.registerBean("rba", EmptyBodyAdvice.class);
278+
this.webAppContext.refresh();
279+
280+
HandlerMethod handlerMethod = handlerMethod(new TestController(), "handleBody", Map.class);
281+
this.handlerAdapter.afterPropertiesSet();
282+
this.handlerAdapter.handle(this.request, this.response, handlerMethod);
283+
284+
assertThat(this.response.getStatus()).isEqualTo(200);
285+
assertThat(this.response.getContentAsString()).isEqualTo("Body: {foo=bar}");
286+
}
287+
274288
private HandlerMethod handlerMethod(Object handler, String methodName, Class<?>... paramTypes) throws Exception {
275289
Method method = handler.getClass().getDeclaredMethod(methodName, paramTypes);
276290
return new InvocableHandlerMethod(handler, method);
@@ -284,7 +298,7 @@ private void assertMethodProcessorCount(int resolverCount, int initBinderResolve
284298

285299

286300
@SuppressWarnings("unused")
287-
private static class SimpleController {
301+
private static class TestController {
288302

289303
@ModelAttribute
290304
public void addAttributes(Model model) {
@@ -296,14 +310,17 @@ public String handle() {
296310
}
297311

298312
public ResponseEntity<Map<String, String>> handleWithResponseEntity() {
299-
return new ResponseEntity<>(Collections.singletonMap(
300-
"foo", "bar"), HttpStatus.OK);
313+
return new ResponseEntity<>(Collections.singletonMap("foo", "bar"), HttpStatus.OK);
301314
}
302315

303316
public ResponseEntity<String> handleBadRequest() {
304317
return new ResponseEntity<>("body", HttpStatus.BAD_REQUEST);
305318
}
306319

320+
@ResponseBody
321+
public String handleBody(@Nullable @RequestBody Map<String, String> body) {
322+
return "Body: " + body;
323+
}
307324
}
308325

309326

@@ -360,6 +377,7 @@ public void addAttributes(Model model) {
360377
}
361378
}
362379

380+
363381
/**
364382
* This class additionally implements {@link RequestBodyAdvice} solely for the purpose
365383
* of verifying that controller advice implementing both {@link ResponseBodyAdvice}
@@ -368,7 +386,8 @@ public void addAttributes(Model model) {
368386
* @see <a href="https://github.com/spring-projects/spring-framework/pull/22638">gh-22638</a>
369387
*/
370388
@ControllerAdvice
371-
private static class ResponseCodeSuppressingAdvice extends AbstractMappingJacksonResponseBodyAdvice implements RequestBodyAdvice {
389+
private static class ResponseCodeSuppressingAdvice
390+
extends AbstractMappingJacksonResponseBodyAdvice implements RequestBodyAdvice {
372391

373392
@Override
374393
protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer, MediaType contentType,
@@ -405,12 +424,42 @@ public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodPa
405424
}
406425

407426
@Override
408-
public Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter,
427+
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
409428
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
410429

411430
return "default value for empty body";
412431
}
432+
}
433+
434+
435+
@ControllerAdvice
436+
private static class EmptyBodyAdvice implements RequestBodyAdvice {
437+
438+
@Override
439+
public boolean supports(MethodParameter param, Type targetType, Class<? extends HttpMessageConverter<?>> type) {
440+
return true;
441+
}
442+
443+
@Override
444+
public HttpInputMessage beforeBodyRead(HttpInputMessage message, MethodParameter param,
445+
Type targetType, Class<? extends HttpMessageConverter<?>> type) {
446+
447+
throw new UnsupportedOperationException();
448+
}
413449

450+
@Override
451+
public Object afterBodyRead(Object body, HttpInputMessage message, MethodParameter param,
452+
Type targetType, Class<? extends HttpMessageConverter<?>> type) {
453+
454+
throw new UnsupportedOperationException();
455+
}
456+
457+
@Override
458+
public Object handleEmptyBody(Object body, HttpInputMessage message, MethodParameter param,
459+
Type targetType, Class<? extends HttpMessageConverter<?>> type) {
460+
461+
return Maps.of("foo", "bar");
462+
}
414463
}
415464

416465
}

0 commit comments

Comments
 (0)