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:

  1. reader.readMono(type, type, (ServerHttpRequest) message, response, context.hints()) - When the BodyExtractor.Context#serverResponse is not empty, which is the case when using ServerRequest. This case is correct.
  2. reader.readMono(type, message, context.hints()) - When the BodyExtractor.Context#serverResponse is empty which is the case for a ClientResponse

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