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)

Comment From: NadChel

It's kind of off-topic, but why do Gradle tests take forever to instantiate? Literally five minutes or so. My laptop is old, but I didn't have this problem when I contributed to Spring Cloud Gateway that doesn't have Gradle

Comment From: poutsma

Superseded in favor of #32622.

Comment From: NadChel

Superseded in favor of #32622.

Are issues superseded by PRs? Aren't they closed by PRs and superseded by other issues?

Comment From: poutsma

Thank you for your suggestion on how we should handle our issue tracker. We will take it into consideration.