diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc index 4f24f60527..cbfcad7a7c 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc @@ -605,9 +605,4 @@ For method parameters and returns values, generally, `@HttpExchange` supports a subset of the method parameters that `@RequestMapping` does. Notably, it excludes any server-side specific parameter types. For details, see the list for xref:integration/rest-clients.adoc#rest-http-interface-method-parameters[@HttpExchange] and -xref:web/webflux/controller/ann-methods/arguments.adoc[@RequestMapping]. - -`@HttpExchange` also supports a `headers()` parameter which accepts `"name=value"`-like -pairs like in `@RequestMapping(headers={})` on the client side. On the server side, -this extends to the full syntax that -xref:#webflux-ann-requestmapping-params-and-headers[`@RequestMapping`] supports. +xref:web/webflux/controller/ann-methods/arguments.adoc[@RequestMapping]. \ No newline at end of file diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java index 819667c744..eaed346dd6 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java @@ -22,7 +22,6 @@ import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; @@ -160,7 +159,7 @@ final class HttpServiceMethod { private record HttpRequestValuesInitializer( @Nullable HttpMethod httpMethod, @Nullable String url, @Nullable MediaType contentType, @Nullable List acceptMediaTypes, - @Nullable MultiValueMap otherHeaders, + MultiValueMap headers, Supplier requestValuesSupplier) { public HttpRequestValues.Builder initializeRequestValuesBuilder() { @@ -177,16 +176,8 @@ final class HttpServiceMethod { if (this.acceptMediaTypes != null) { requestValues.setAccept(this.acceptMediaTypes); } - if (this.otherHeaders != null) { - this.otherHeaders.forEach((name, values) -> { - if (values.size() == 1) { - requestValues.addHeader(name, values.get(0)); - } - else { - requestValues.addHeader(name, values.toArray(new String[0])); - } - }); - } + this.headers.forEach((name, values) -> + values.forEach(value -> requestValues.addHeader(name, value))); return requestValues; } @@ -217,10 +208,10 @@ final class HttpServiceMethod { String url = initUrl(typeAnnotation, methodAnnotation, embeddedValueResolver); MediaType contentType = initContentType(typeAnnotation, methodAnnotation); List acceptableMediaTypes = initAccept(typeAnnotation, methodAnnotation); - MultiValueMap headers = initHeaders(typeAnnotation, methodAnnotation, - embeddedValueResolver); - return new HttpRequestValuesInitializer(httpMethod, url, contentType, - acceptableMediaTypes, headers, requestValuesSupplier); + MultiValueMap headers = initHeaders(typeAnnotation, methodAnnotation, embeddedValueResolver); + + return new HttpRequestValuesInitializer( + httpMethod, url, contentType, acceptableMediaTypes, headers, requestValuesSupplier); } @Nullable @@ -296,48 +287,42 @@ final class HttpServiceMethod { return null; } - private static MultiValueMap parseHeaders(String[] headersArray, + private static MultiValueMap initHeaders( + @Nullable HttpExchange typeAnnotation, HttpExchange methodAnnotation, @Nullable StringValueResolver embeddedValueResolver) { + MultiValueMap headers = new LinkedMultiValueMap<>(); - for (String h: headersArray) { - String[] headerPair = StringUtils.split(h, "="); - if (headerPair != null) { - String headerName = headerPair[0].trim(); - List headerValues = new ArrayList<>(); - Set parsedValues = StringUtils.commaDelimitedListToSet(headerPair[1]); - for (String headerValue : parsedValues) { - if (embeddedValueResolver != null) { - headerValue = embeddedValueResolver.resolveStringValue(headerValue); - } - if (headerValue != null) { - headerValue = headerValue.trim(); - headerValues.add(headerValue); - } - } - if (!headerValues.isEmpty()) { - headers.addAll(headerName, headerValues); - } - } + if (typeAnnotation != null) { + addHeaders(typeAnnotation.headers(), embeddedValueResolver, headers); } + addHeaders(methodAnnotation.headers(), embeddedValueResolver, headers); return headers; } - @Nullable - private static MultiValueMap initHeaders(@Nullable HttpExchange typeAnnotation, HttpExchange methodAnnotation, - @Nullable StringValueResolver embeddedValueResolver) { - MultiValueMap methodLevelHeaders = parseHeaders(methodAnnotation.headers(), - embeddedValueResolver); - if (!ObjectUtils.isEmpty(methodLevelHeaders)) { - return methodLevelHeaders; - } + private static void addHeaders( + String[] rawValues, @Nullable StringValueResolver embeddedValueResolver, + MultiValueMap outputHeaders) { - MultiValueMap typeLevelHeaders = (typeAnnotation != null ? - parseHeaders(typeAnnotation.headers(), embeddedValueResolver) : null); - if (!ObjectUtils.isEmpty(typeLevelHeaders)) { - return typeLevelHeaders; + for (String rawValue: rawValues) { + String[] pair = StringUtils.split(rawValue, "="); + if (pair == null) { + continue; + } + String name = pair[0].trim(); + List values = new ArrayList<>(); + for (String value : StringUtils.commaDelimitedListToSet(pair[1])) { + if (embeddedValueResolver != null) { + value = embeddedValueResolver.resolveStringValue(value); + } + if (value != null) { + value = value.trim(); + values.add(value); + } + } + if (!values.isEmpty()) { + outputHeaders.addAll(name, values); + } } - - return null; } private static List getAnnotationDescriptors(AnnotatedElement element) { diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTests.java index 90932f856b..012d70f74c 100644 --- a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTests.java +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -348,8 +348,10 @@ class HttpServiceMethodTests { @PostExchange(url = "/url", contentType = APPLICATION_JSON_VALUE, accept = APPLICATION_JSON_VALUE) void performPost(); - @HttpExchange(contentType = APPLICATION_JSON_VALUE, headers = {"CustomHeader=a,b, c", - "Content-Type=" + APPLICATION_NDJSON_VALUE}, method = "GET") + @HttpExchange( + method = "GET", + contentType = APPLICATION_JSON_VALUE, + headers = {"CustomHeader=a,b, c", "Content-Type=" + APPLICATION_NDJSON_VALUE}) void performGetWithHeaders(); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java index 8e438acd3f..fc305815db 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java @@ -430,7 +430,8 @@ class RequestMappingHandlerMappingTests { @PostExchange(url = "/custom", contentType = "application/json", accept = "text/plain;charset=UTF-8") public void customValuesExchange(){} - @HttpExchange(method="GET", url = "/headers", + @HttpExchange( + method="GET", url = "/headers", headers = {"h1=hv1", "!h2", "Accept=application/ignored"}) public String customHeadersExchange() { return "info";