I'm trying to upgrade from Spring-Boot 3.3.13 to 3.4.6 and noticed that there has been a change in how @ModelAttribute or the passing of them in request methods work.

Example request:

/stream/proxy.m3u8?uuid=5aabe0d8-d17b-4212-aabd-acf6eddfb4c0&headers.Origin.0=https%3A%2F%2Fwww.example.com&headers.Referer.0=https%3A%2F%2Fwww.example.com%2F

My code:

  @ModelAttribute("headersFromQuery")
  public HttpHeaders buildHeadersFromQuery(@RequestParam Map<String, String> query) {
    return httpConverterService.buildHeadersFromQuery(query, false);
  }

  @ModelAttribute("cookiesFromQuery")
  public Map<String, String> buildCookiesFromQuery(@RequestParam Map<String, String> query) {
    return httpConverterService.buildCookiesFromQuery(query);
  }

  @GetMapping(path = "/proxy.{mode:.+}", params = { "uuid" })
  public Mono<Void> proxy(@PathVariable String mode, @RequestParam UUID uuid,
      @ModelAttribute("headersFromQuery") HttpHeaders httpHeaders,
      @ModelAttribute("cookiesFromQuery") Map<String, String> cookies,
      @RequestAttribute ProxiedRequestUnit proxiedRequestUnit, ServerHttpResponse serverHttpResponse) {
    return proxy(mode, AbstractPlaylistProvider.getRealUri(uuid), httpHeaders, cookies, proxiedRequestUnit, serverHttpResponse);
  }

With 3.3.13 httpHeaders contained the two entries that came with the request and were dealt with in buildHeadersFromQuery(). With all 3.4.x it seems as if Spring-Boot tries to merge those two entries with the incoming HTTP-headers into httpHeaders. I'm not sure, though, because I can't check the result of this as there is a validation error. With 3.4.6:

    ... o.s.w.s.adapter.HttpWebHandlerAdapter    : [2ef1a757-16] HTTP GET "/stream/proxy.m3u8?uuid=5aabe0d8-d17b-4212-aabd-acf6eddfb4c0&headers.Origin.0=https%3A%2F%2Fwww.example.com&headers.Referer.0=https%3A%2F%2Fwww.example.com%2F"
    ... s.w.r.r.m.a.RequestMappingHandlerMapping : [2ef1a757-16] Mapped to com.myproject.controller.StreamController#proxy(String, UUID, HttpHeaders, Map, ProxiedRequestUnit, ServerHttpResponse)
    ... org.springframework.web.HttpLogging      : [2ef1a757-16] Resolved [WebExchangeBindException: Validation failed for argument at index 2 in method: public reactor.core.publisher.Mono<java.lang.Void> com.myproject.controller.StreamController.proxy(java.lang.String,java.util.UUID,org.springframework.http.HttpHeaders,java.util.Map<java.lang.String, java.lang.String>,com.myproject.service.ProxiedRequestUnit,org.springframework.http.server.reactive.ServerHttpResponse), with 1 error(s): [Field error in object 'headersFromQuery' on field 'acceptLanguage': rejected value [de-AT,de;q=0.9,en-GB;q=0.8,en;q=0.7]; codes [typeMismatch.headersFromQuery.acceptLanguage,typeMismatch.acceptLanguage,typeMismatch.java.util.List,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [headersFromQuery.acceptLanguage,acceptLanguage]; arguments []; default message [acceptLanguage]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.util.List' for property 'acceptLanguage'; Failed to convert from type [java.lang.String] to type [java.util.Locale$LanguageRange] for value [de-AT,de;q=0.9,en-GB;q=0.8,en;q=0.7]]] ] for HTTP GET /stream/proxy.m3u8

In earlier 3.4.x versions it also complained about the "Host"-header.

This is a bug in all 3.4.x as my intention is to use the model attribute as I built it with the build*-methods.

As far as the conversion error is concerned, I don't know what to do with that. It clearly is a normal "Accept-Language"-header, but at this point I don't care about it. It's more important to use the model attributes.

Comment From: rstoyanchev

This is likely related to #32676 where headers are now included in the data binding values. We exclude some well known headers that tend to interfere but you can further customize this as shown in https://github.com/spring-projects/spring-framework/issues/34182#issuecomment-2575598084.

Comment From: vermgit

I don't really think so. In my code from above my intention is to bind a specific model attribute to a parameter. In this case any further background logic would interfere with my expectation.

I'm with you if there'd only be the name of the parameter or the type of it and Spring would have to figure out itself what I meant. Like if I simply had HttpHeaders httpHeaders as the third parameter, Spring could interpret it as "ah, an HttpHeader, so let me collect all HTTP-headers, fill a new object with that and pass that to the request handler".

However, the @ModelAttribute plus name (so no lookup based on the type is needed) should bind it tightly to the result of the method writing that very model attribute, more so tightly as it's in the same class. In a way I'm "trying to help" Spring here and that particular object should be read-only for Spring, i.e. I should be able to modify it in my request handler if I wanted to.

Maybe if I wanted my self-built header and Spring's generated header I'd want to specify both as parameters:

@GetMapping(path = "/proxy.{mode:.+}", params = { "uuid" })
public Mono<Void> proxy(@PathVariable String mode, @RequestParam UUID uuid,
  @ModelAttribute("headersFromQuery") HttpHeaders httpHeaders,
  HttpHeaders requestHttpHeaders,                                        // <-- Spring's
  ...

At least that's how I'd see it :)

Comment From: rstoyanchev

I don't really think so

It's not obvious to me how this connects to my comment. Do you mean you don't think the problem is related to the issue I linked?

You're a bit ahead of me as I haven't yet grasped what you're trying to do. What the code snippets show is a bit unusual, not saying it's wrong, just that it's not obvious to me what the goal or use case is. There is also some missing detail with the httpConverterService not included.

Please provide a small sample that demonstrates the case.