version

  • Spring Boot 2.6.2

code

@SpringBootApplication
class FluxErrorHandlingApplication

fun main(args: Array<String>) {
    runApplication<FluxErrorHandlingApplication>(*args)
}

@Configuration
class Config {
    @Bean
    fun router(handler: Handler) =
        org.springframework.web.reactive.function.server.router {
            GET("/error", handler::handle)
        }
}

@Component
class Handler {
    fun handle(request: ServerRequest): Mono<ServerResponse> {
        throw RuntimeException()
    }
}

result

curl -v localhost:8080/error                                                                                                                                                                             master ✭ ✚ ✱
*   Trying 127.0.0.1:8080...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /error HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.68.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 500 Internal Server Error
< Content-Type: application/json
< Content-Type: application/json
< Content-Length: 131

Comment From: wilkinsona

Here's a more minimal sample that takes Kotlin out of the picture:

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;

import reactor.core.publisher.Mono;

import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;

@SpringBootApplication
public class Gh29179Application {

    public static void main(String[] args) {
        SpringApplication.run(Gh29179Application.class, args);
    }


    @Configuration
    class Config {
        @Bean
        RouterFunction<ServerResponse> router(Handler handler) {
            return route(GET("/"), handler::handle);
        }
    }

    @Component
    class Handler {
        Mono<ServerResponse> handle(ServerRequest request) {
            throw new RuntimeException();
        }
    }

}

The duplicate header remains:

$ curl -i localhost:8080
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
Content-Type: application/json
Content-Length: 126

{"timestamp":"2022-01-04T16:05:38.216+00:00","path":"/","status":500,"error":"Internal Server Error","requestId":"13e4496c-1"}

Comment From: wilkinsona

This appears to be a regression in Spring Framework 5.3.14. Downgrading to 5.3.13 results in a single Content-Type header. I believe that https://github.com/spring-projects/spring-framework/commit/2a5713f389e305300d7659887d38a89aa4e7e91e is the cause.

Comment From: ryanb93

Combined with https://github.com/istio/istio/issues/30470 this causes clients to receive Content-Type: application/json,application/json;charset=UTF-8 - which clients fail to parse.

Comment From: poutsma

The cause here seems to be in NettyHeadersAdapter, which implements putAll by adding headers instead of setting (and overwriting) them.

This bug seems to have been in NettyHeadersAdapter since the type was introduced, and the changes in https://github.com/spring-projects/spring-framework/commit/2a5713f389e305300d7659887d38a89aa4e7e91e only brought them to light.