From 0996889313884efae127c8f825068f538fc09cd4 Mon Sep 17 00:00:00 2001 From: kdelay Date: Tue, 6 Oct 2026 15:42:16 +0900 Subject: [PATCH] Do not decode the body of a ResponseEntity ResponseEntityDecoder passed Void.class on to the delegate decoder, so a ResponseEntity or HttpEntity method failed with UnknownContentTypeException when the server sent a text/plain or text/html body. RestTemplate and RestClient skip the body for Void response entities; do the same here and keep the status and headers. Signed-off-by: kdelay --- .../support/ResponseEntityDecoder.java | 3 ++ .../cloud/openfeign/SpringDecoderTests.java | 45 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/ResponseEntityDecoder.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/ResponseEntityDecoder.java index cd9762e1f..ffbbc221d 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/ResponseEntityDecoder.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/ResponseEntityDecoder.java @@ -51,6 +51,9 @@ public Object decode(final Response response, Type type) throws IOException, Fei if (isParameterizeHttpEntity(type)) { type = ((ParameterizedType) type).getActualTypeArguments()[0]; + if (type == Void.class) { + return createResponse(null, response); + } Object decodedObject = this.decoder.decode(response, type); return createResponse(decodedObject, response); diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/SpringDecoderTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/SpringDecoderTests.java index 8814c4c07..f224fb886 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/SpringDecoderTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/SpringDecoderTests.java @@ -16,6 +16,12 @@ package org.springframework.cloud.openfeign; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.List; +import java.util.Map; + import feign.Request; import feign.Response; import org.junit.jupiter.api.BeforeEach; @@ -24,8 +30,14 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.cloud.loadbalancer.support.SimpleObjectProvider; import org.springframework.cloud.openfeign.support.FeignHttpMessageConverters; +import org.springframework.cloud.openfeign.support.ResponseEntityDecoder; import org.springframework.cloud.openfeign.support.SpringDecoder; +import org.springframework.core.ResolvableType; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.mockito.Mockito.mock; @@ -53,4 +65,37 @@ void shouldNotThrownNPEWhenNoContent() { .doesNotThrowAnyException(); } + @Test + void shouldNotReadBodyForResponseEntityOfVoid() throws Exception { + Type type = ResolvableType.forClassWithGenerics(ResponseEntity.class, Void.class).getType(); + + Object decoded = new ResponseEntityDecoder(decoder).decode(textResponse(), type); + + assertThat(decoded).isInstanceOf(ResponseEntity.class); + ResponseEntity entity = (ResponseEntity) decoded; + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); + assertThat(entity.getHeaders().getFirst("Location")).isEqualTo("/jobs/1"); + assertThat(entity.getBody()).isNull(); + } + + @Test + void shouldNotReadBodyForHttpEntityOfVoid() throws Exception { + Type type = ResolvableType.forClassWithGenerics(HttpEntity.class, Void.class).getType(); + + Object decoded = new ResponseEntityDecoder(decoder).decode(textResponse(), type); + + assertThat(decoded).isInstanceOf(ResponseEntity.class); + assertThat(((HttpEntity) decoded).getBody()).isNull(); + } + + private static Response textResponse() { + return Response.builder() + .request(mock(Request.class)) + .status(202) + .headers(Map.>of("Content-Type", List.of("text/plain"), "Location", + List.of("/jobs/1"))) + .body("Accepted", StandardCharsets.UTF_8) + .build(); + } + }