Description
I was examining this Spring Cloud Gateway issue and discovered that its resolution is blocked by the fact that org.springframework.http.codec.EncoderHttpMessageWriter
adds a default Content-Type
header anyway, regardless of whether the body Publisher
is empty or not. Here's an MRE
package org.springframework.cloud.gateway._misc;
import java.util.Collections;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.CharSequenceEncoder;
import org.springframework.core.codec.Encoder;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.codec.EncoderHttpMessageWriter;
import org.springframework.web.server.ServerWebExchange;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Answers.RETURNS_DEEP_STUBS;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
public class GenericTest {
@Test
public void test() {
Encoder<CharSequence> encoder = CharSequenceEncoder.textPlainOnly();
EncoderHttpMessageWriter<CharSequence> writer = new EncoderHttpMessageWriter<>(encoder);
ServerWebExchange exchangeMock = mock(ServerWebExchange.class, RETURNS_DEEP_STUBS);
given(exchangeMock.getResponse().bufferFactory()).willReturn(DefaultDataBufferFactory.sharedInstance);
ReactiveHttpOutputMessage outputMessage = new CachedBodyOutputMessage(exchangeMock,
HttpHeaders.writableHttpHeaders(HttpHeaders.EMPTY));
Mono<Void> mono = writer.write(Mono.empty(), ResolvableType.forClass(String.class),
null, outputMessage, Collections.emptyMap());
StepVerifier.create(mono)
.verifyComplete();
assertThat(outputMessage.getHeaders()).doesNotContainKey(HttpHeaders.CONTENT_TYPE);
}
}
To better match the original issue, I used org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage
, but I believe it doesn't really matter. You can replace it with e.g. org.springframework.mock.http.client.reactive.MockClientHttpRequest
, and it will still be reproducible
ReactiveHttpOutputMessage outputMessage = new MockClientHttpRequest(HttpMethod.POST, "/");
The problem is updateContentType(..)
which sets a Content-Type
header
@Override
public Mono<Void> write(Publisher<? extends T> inputStream, ResolvableType elementType,
@Nullable MediaType mediaType, ReactiveHttpOutputMessage message, Map<String, Object> hints) {
MediaType contentType = updateContentType(message, mediaType);
Flux<DataBuffer> body = this.encoder.encode(
inputStream, message.bufferFactory(), elementType, contentType, hints);
@Nullable
private MediaType updateContentType(ReactiveHttpOutputMessage message, @Nullable MediaType mediaType) {
MediaType result = message.getHeaders().getContentType();
if (result != null) {
return result;
}
MediaType fallback = this.defaultMediaType;
result = (useFallback(mediaType, fallback) ? fallback : mediaType);
if (result != null) {
result = addDefaultCharset(result, fallback);
message.getHeaders().setContentType(result);
}
return result;
}
Spring Cloud Gateway currently uses Spring Web 6.1.5
You may assign me to this issue if you want. I wrote a straightforward fix and added an extra test (will formally submit a PR soon)