Here's a failing test:
// these are pretty basic dependencies so I won't include imports or pom
@Test
void testIfNoHeadersAreAddedToRequestImplicitly() {
Mono<Map<String, Object>> responseMono = WebClient.builder()
.baseUrl("https://httpbin.org")
.build()
.get()
.uri("/headers")
.retrieve()
.bodyToMono(new ParameterizedTypeReference<>() {});
StepVerifier.create(responseMono.map(m -> m.get("headers")).cast(Map.class))
.expectNextMatches(Map::isEmpty)
.verifyComplete();
}
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<!-- ... -->
<properties>
<java.version>17</java.version>
<spring-cloud.version>2022.0.4</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</dependency>
<!-- ↓ includes WebFlux starter -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock-standalone</artifactId>
<version>3.5.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
I'm pretty sure WebClient adds some default request headers, such as HOST, and I want to see where it happens. It so happens I need to disable this feature, but I'm just as eager to see the cause
So far, my debugging of this issue was unsuccessful – including debugging of DefaultWebClient.exchange() which seems to work as expected
However, I discovered that a wrong request is passed to this callback
// org.springframework.http.client.reactive.ReactorClientHttpConnector.connect(..)
return requestSender
// put a breakpoint on the lambda
.send((request, outbound) -> requestCallback.apply(adaptRequest(method, uri, request, outbound)))
When I dug deeper, Netty just pretended those headers were all along (once any request appears, it already has those default headers)
The issue is reproduced with WireMock too so it's unlikely to have anything to do with httpbin (btw, in case you're unfamiliar with it, you may visit the API's page)
The default headers are explicitly added by Netty when a request is prepared for sending. The main point of interest is here:
As you might know, Spring's
WebClientuses Project Reactor and Netty under the hood for making web requests. When you create aWebClientinstance and build a request with it, the headers do not appear immediately. However, once the request is about to be sent out (when Netty'sHttpClientOperationscomes into play), Netty adds these default headers (like HOST, Accept-Encoding, etc) even if none were explicitly set by you.Here's a look into part of
HttpClientOperationsfrom Netty on GitHub:The
HttpRequestEncoder.encoderHeader(...)part is where the headers are prepared and added if they don't exist. You might take a look at theencodeHeadersmethod too for deeper insights.Here is the point-in-time in
HttpClientOperationswhere headers are added:Unfortunately,
WebClientdoes not provide an out-of-the-box solution to prevent Netty from adding these headers. However, you can create a custom filter and add it to yourWebClientto intentionally remove these headers each time a request is built:This way, each time you build a request, the filter will remove the Host header (and others if you wish) before sending it off. Please note that this might not be the best practice as some servers might require the
Hostheader or others for processing the request.