Affects: spring-web-6.1.1 / spring-boot-starter-web:3.2.0

We are adding headers to WebClient requests, which sometimes have value (some optional trace IDs).

Prior to SpringBoot 3.2, those headers were simply ignored. This behaviour has changed. Now, an IllegalArgumentException is thrown.

Spring applies headers here:

org.springframework.http.client.reactive.JettyClientHttpRequest#applyHeaders:

protected void applyHeaders() {
    HttpHeaders headers = getHeaders();
    this.jettyRequest.headers(fields -> {
        headers.forEach((key, value) -> value.forEach(v -> fields.add(key, v))); <<<<<<<<<------
        if (!headers.containsKey(HttpHeaders.ACCEPT)) {
            fields.add(HttpHeaders.ACCEPT, "*/*");
        }
    });
}

fields.add(...) uses

org.eclipse.jetty.http.HttpFields.Mutable#add(java.lang.String, java.lang.String) of jetty-http-12.0.3.jar

    default Mutable add(String name, String value)
    {
        if (value == null)
            throw new IllegalArgumentException("null value");
        return add(new HttpField(name, value));
    }

This implementation seems to have changed. In the past, this worked, because jetty-http-11.0.17.jar has following implementation:

org.eclipse.jetty.http.HttpFields.Mutable#add(java.lang.String, java.lang.String)

    public Mutable add(String name, String value)
    {
        if (value != null)
            return add(new HttpField(name, value));
        return this;
    }

I'm not sure if there was a change in servlet specs (as discussed in https://github.com/jakartaee/servlet/issues/159), if it is a regression in Jetty or if spring-web needs to be updated.

Comment From: snicoll

Spring Boot 3.2 moves to Jetty 12 and I think you've managed to find the root cause of this. I am not sure I understand why this is reported against Spring. Are you saying you'd expect us to not set the header if the value is null or something like that?

Comment From: steinsag

I simply don't know who's "fault" it is? Is Jetty changing the contract or is Spring simply using the wrong API and it was a coincidence that it used to work?

Comment From: snicoll

How do you set a header with a null value for a start?

I've tried with both WebClient and RestClient and any attempt to invoke .header("myHeader", null) leads to an error upfront because the value is null. This seems pretty obvious to me that the API does not allow a null value so I believe what you have was working by accident.

Comment From: steinsag

With something like:

    Map<String, String> someHeaders = new HashMap<>();
    someHeaders.put("X-TRACE-ID", null);
    someHeaders.put("X-SOME-OTHER-FIELD", "some value");

    WebClient build = WebClient.builder().build();
    build.get().uri("http://example.com/").headers(
            httpHeaders -> someHeaders.forEach(httpHeaders::add)
    );

Comment From: poutsma

As indicated, this is a change made in Jetty 12, where null values are no longer silently ignored. If you would like to see this regression reverted, then please file an issue with the Jetty project.

FWIW, most of the other HTTP clients we support do not accept null values either. This includes the JDK HttpClient, as well as the Reactor and OkHttp clients. The Apache HttpClient treats a null value as an empty value, which is quite different than silently ignoring them.

Ideally, HttpHeaders would throw an exception when supplied with a null value, but since it is used on both the client and server side, where there are different constraint with regard to header values, we are limited in what we can do.