With #22267 the ResourceDecoder was extended to support a filename hint that allows setting the filename of the Resource. However, this does not work correctly with using the Spring WebClient.
e.g.
ResponseEntity<ByteArrayResource> response = webClient.get("/some-resource)
.flatMap(response -> response.toEntity(ByteArrayResource.class))
.block();
The resource returned by this response will have null when invoking getFileName().
The reason for that is the following:
When invoking response.ToEntity(ByteArrayResource.class). We will reach:
https://github.com/spring-projects/spring-framework/blob/87c3bb579739aa4892f3b647f8eef7e2dc16a7f5/spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyExtractors.java#L83-L89
Which calls readToMono:
https://github.com/spring-projects/spring-framework/blob/87c3bb579739aa4892f3b647f8eef7e2dc16a7f5/spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyExtractors.java#L206-L211
There are now 2 possibilities for invoking the reader:
reader.readMono(type, type, (ServerHttpRequest) message, response, context.hints())- When theBodyExtractor.Context#serverResponseis not empty, which is the case when usingServerRequest. This case is correct.reader.readMono(type, message, context.hints())- When theBodyExtractor.Context#serverResponseis empty which is the case for aClientResponse
Since we are using WebClient we will skip the analysis for 1. And go to 2.
The serverResponse on the extractor is set to empty here:
https://github.com/spring-projects/spring-framework/blob/72895f081026df7e0b34807729d9cdea6c7ff4ec/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java#L82-L97
When we look into the implementation of reader.readMono(type, message, context.hints()). More precisely the implementation of https://github.com/spring-projects/spring-framework/blob/72895f081026df7e0b34807729d9cdea6c7ff4ec/spring-web/src/main/java/org/springframework/http/codec/HttpMessageReader.java#L76
In DecoderHttpMessageReader:
https://github.com/spring-projects/spring-framework/blob/dbec16d566ed7ac8a946e0d832d2a52d2d559f91/spring-web/src/main/java/org/springframework/http/codec/DecoderHttpMessageReader.java#L102-L106
We can see that the hints are passed as is without creating new hints based on the message.
The fix would be to also get new hints from ReactiveHttpInputMessage when reading a flux or mono without a ServerHttpResponse.
More precisely in this location:
https://github.com/spring-projects/spring-framework/blob/dbec16d566ed7ac8a946e0d832d2a52d2d559f91/spring-web/src/main/java/org/springframework/http/codec/DecoderHttpMessageReader.java#L96-L106
This can be done only for the ResourceHttpMessageReader and not for DecoderHttpMessageReader
Reproducable Test case
/**
* Unit tests for {@link ResourceHttpMessageReader}.
*
* @author Filip Hrisafov
*/
public class ResourceHttpMessageReaderTests extends AbstractLeakCheckingTests {
private final ResourceHttpMessageReader reader = new ResourceHttpMessageReader();
@Test
void readResourceAsMono() throws IOException {
String body = "Test resource content";
MockServerHttpRequest request = request(body, "test.txt");
Resource result = this.reader.readMono(ResolvableType.forClass(ByteArrayResource.class), request, null)
.single()
.block();
assertThat(result).isNotNull();
assertThat(result.getFilename()).isEqualTo("test.txt");
try (InputStream stream = result.getInputStream()) {
assertThat(stream).hasContent(body);
}
}
private MockServerHttpRequest request(String body, String filename) {
return request(Mono.just(stringBuffer(body)), filename);
}
private MockServerHttpRequest request(Publisher<? extends DataBuffer> body, String filename) {
return MockServerHttpRequest
.method(HttpMethod.GET, "/")
.header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE)
.header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.builder("attachment")
.name("file")
.filename(filename)
.build()
.toString())
.body(body);
}
private DataBuffer stringBuffer(String value) {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = this.bufferFactory.allocateBuffer(bytes.length);
buffer.write(bytes);
return buffer;
}
}
I am not providing a PR, because I am not sure how you would best like this to be fixed.
Edit: I submitted the issue before having the chance to clean it up.
Comment From: rstoyanchev
Indeed we have slightly richer context on the server side where we tend to extract hints from controller method signatures. However hints could also be extracted from headers as in this case.
I experimented with this additional protected method in DecoderHttpMessageReader which can then be invoked from the decode methods with just a ReactiveHttpInputMessage:
protected Map<String, Object> getReadHints(ResolvableType elementType, ReactiveHttpInputMessage message) {
return Hints.none();
}
This can then be overridden in ResourceHttpMessageReader and that seems to work, so I'll put this together for 5.3.
Comment From: filiphr
Thanks a lot for doing the enhancement @rstoyanchev