We have separate HandlerMethodReturnValueHandler
s for regular response body vs streaming type return values, and if you declare a controller method to return ResponseEntity<?>
, with an intentionally opaque generic type (so you can either stream or not), we can't correctly handle it.
ReturnValueMethodParameter
does consider the actual return value Class
, and that helps with cases like SseEmitter
vs some Object <T>
, but doesn't help in cases like ResponseEntity<?>
where you would also need generic type info.
We can consider improved support through a consolidated return value handler for the ResponseEntity
wrapper that delegates to either the one for response body or the one for streaming after considering the actual body in the ResponseEntity
.
Comment From: DeepDhamala
Hi! 👋
After doing some digging into the internals, I think I understand the issue like this and have an idea on what can be done—please correct me in case I’m overlooking something.
When the HandlerMethodReturnValueHandlerComposite
method selectHandler(...)
iterates through the available HandlerMethodReturnValueHandler
implementations, the one we expect to handle a return type(actual return value) of ResponseEntity<AnyEmitter>
is ResponseBodyEmitterReturnValueHandler
. However, the supportsReturnType(...)
method in ResponseBodyEmitterReturnValueHandler
fails to match because it attempts to resolve the body type from the method signature .
Class<?> bodyType = ResponseEntity.class.isAssignableFrom(returnType.getParameterType()) ?
ResolvableType.forMethodParameter(returnType).getGeneric().resolve() :
returnType.getParameterType();
In this case, the method signature is declared as ResponseEntity<?>, as a result, the resolved bodyType
is null
, and the handler does not recognize it as a supported return type.
@Override
public boolean supportsReturnType(MethodParameter returnType) {
Class<?> bodyType = ResponseEntity.class.isAssignableFrom(returnType.getParameterType()) ?
ResolvableType.forMethodParameter(returnType).getGeneric().resolve() :
returnType.getParameterType();
return (bodyType != null && (ResponseBodyEmitter.class.isAssignableFrom(bodyType) ||
this.reactiveHandler.isReactiveType(bodyType)));
}
In the HandlerMethodReturnValueHandlerComposite
’s selectHandler(...)
method, we already have access to the actual return object via the value
parameter.
@Nullable
private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
boolean isAsyncValue = isAsyncReturnValue(value, returnType);
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
continue;
}
if (handler.supportsReturnType(returnType)) {
return handler;
}
}
return null;
}
Would it make sense to also consider the Object value
(i.e. the actual return value) in order to determine the body type dynamically and delegate accordingly?
Comment From: rstoyanchev
@DeepDhamala yes one way or another the value needs to be considered, and typically is once a return value handler is selected. The problem is that in this case there are two potential handlers.
I've made a more targeted change to address the issue.
Comment From: DeepDhamala
Glad to see how it's implemented! The use of a wrapper ResponseEntityReturnValueHandler
that delegates to ResponseBodyEmitterReturnValueHandler
based on the runtime type of the body, or falls back to HttpEntityMethodProcessor
.