Codersee

How To Follow Redirects (3XX) With Spring WebClient?

The image if a featured image of the post titled How To Follow Redirects (3XX) With Spring WebClient and shows Spring Boot logo in the foreground and a blurred image of a programmers hand in the background.

1. Introduction

Hello friend! If you see this post, then you are either a regular visitor of my blog or you’ve already received your first 3XX response and realized that it’s time to follow redirects with Spring WebClient 😀

Regardless of the answer, I can assure you that you’ve come to the right place, and after reading this article you will know precisely how to implement a redirect feature with dedicated HttpClient functionality.

Before we start, I just want you to know that although the examples are written in Kotlin, you should be able to this logic regardless of the language used in your Spring Boot / WebFlux project.

2. followRedirect() – a Simple Way to Follow Redirects with Spring WebClient

So let’s start everything by introducing HttpClient’s method- followRedirect()– which we will make use of today. Of course, this is not the only solution to tackle the 3XX response case in Spring WebFlux, but in my opinion, it’s the cleanest way to do so. Moreover, this approach lets us implement our desired logic as a part of the configuration, without polluting the actual business logic.

When we look into the HttpClient implementation, we will spot that the followRedirect() method comes in 6 variants:

HttpClient followRedirect(boolean)
HttpClient followRedirect(boolean, Consumer<HttpClientRequest>)
HttpClient followRedirect(BiPredicate<HttpClientRequest, HttpClientResponse>)
HttpClient followRedirect(boolean followRedirect, BiConsumer<HttpHeaders, HttpClientRequest>)
HttpClient followRedirect(BiPredicate<HttpClientRequest, HttpClientResponse>,BiConsumer<HttpHeaders, HttpClientRequest>)
HttpClient followRedirect(BiPredicate<HttpClientRequest, HttpClientResponse>, Consumer<HttpClientRequest>)

And although these signatures may look scary, you’ll see how simple to implement and useful they are in the upcoming paragraphs.

3. Set Up The Project

As always, let’s start by creating a new Spring Boot project using the Spring Initializr:

The image shows Spring Initializr page with all settings necessary to set up a new Spring WebFlux project.

 

As we can see, the only additional dependency we will need today is the Spring Reactive Web. The rest is totally up to you.

Nextly, let’s generate this project and import it to our favorite IDE (like IntelliJ, for example).

4. Configure WebClient and HttpClient

As the next step to properly follow redirects with Spring WebClient, let’s add a Config class:

@Configuration
class Config {

  companion object {
    private const val BASE_URL = "http://localhost:8090"
  }

  @Bean
  fun webClient(httpClient: HttpClient): WebClient =
    WebClient.builder()
      .clientConnector(ReactorClientHttpConnector(httpClient))
      .baseUrl(BASE_URL)
      .build()

  @Bean
  fun httpClient(): HttpClient =
    HttpClient.create()

}

As we can see, we create here the most basic implementation of the WebClient with a base URL set as http://localhost:8090.
 

Image shows two ebooks people can get for free after joining newsletter

 

5. Create a Service

Nextly, let’s implement the RedirectService responsible for querying particular endpoint:

@Service
class RedirectService(
  private val webClient: WebClient
) {

  fun one(): Mono<ResponseBodyDto> =
    webClient.get()
      .uri("/some-endpoint")
      .header("custom-header", "custom-header-value")
      .header("Authorization", "Bearer value")
      .retrieve()
      .bodyToMono(ResponseBodyDto::class.java)

  data class ResponseBodyDto(val message: String)
}

As we can clearly see, with this implementation we query the http://localhost:8090/some-endpoint with a GET request passing additional custom-header and Authorization headers. The second one is crucial in this tutorial and you’ll know why in the upcoming paragraphs.

Additionally, we expect that the endpoint returns JSON with one item containing a message, which we deserialize into ResponseBodyDto and return as a Mono.

6. Implement Controller

As the next step, let’s add a TestController:

@RestController
@RequestMapping("/test")
class TestController(
private val redirectService: RedirectService
) {

  @GetMapping
  fun one(): Mono<RedirectService.ResponseBodyDto> =
    redirectService.one()

}

As we can see, we expose one GET endpoint, which we will use to test our functionality.

If everything will be working correctly, we should see the 200 OK response with the message when performing GET requests to http://localhost:8080/test.

7. Test Redirect With Default Implementation

Finally, let’s test our functionality. The case is pretty straightforward: each time we perform a GET http://localhost:8090/some-endpoint request, as a response, we get the 301 Status Code without the response body and with Location header set to http://localhost:8090/one-redirect-target.

On the other hand, the http://localhost:8090/one-redirect-target endpoint returns 200 OK with the example message:

curl --location --request GET 'http://localhost:8080/test'

# Response

Status: 200 OK 
Response Body: Empty

As a result, we received the 200 OK status with an empty response body- definitely not something we would like to see.

To be even more specific, our implementation neither throws an error nor returns the correct value- it ends ups with an empty Mono, which we do not handle.

If you are interested in how to set up a “fake” api, just like the one above, then the answer is Mockoon. Let me know in the comments section if you would be interested in a tutorial about it 😉

8. Make Use of the followRedirect(boolean)

With that being done, let’s finally do something to follow the 301 response in our WebClient.

As the first one, let’s start with the followRedirect(boolean) variant:

@Bean
fun httpClient(): HttpClient =
  HttpClient.create()
    .followRedirect(true)

The only thing we have to do in this case is to pass the true to the followRedirect(boolean) method. Per the documentation:

Specifies whether HTTP status 301|302|303|307|308 auto-redirect support is enabled.

So this time, our implementation will follow the redirect properly when any of the above status codes is returned. Moreover, if we check in Mockoon, we will see that both the custom and Authorization headers are present.

But here comes the catch- the sensitive headers:

  • Expect,
  • Cookie,
  • Authorization,
  • Proxy-Authorization,

are only added when redirecting to the same domain! When testing locally, when we simply change the port of a redirected resource from 8090 to another, we will see that the Authorization header is missing.

9. followRedirect- Add Sensitive Headers

OK, so what possibility do we have if we would like to handle redirects to different domains and re-add the same headers?

Well, if this is the only thing we would like to do, then let’s make use of the HttpClient followRedirect(boolean followRedirect, BiConsumer<HttpHeaders, HttpClientRequest>) implementation:

@Bean
fun httpClient(): HttpClient =
  HttpClient.create()
    .followRedirect(true) { headers, request ->
      request.headers(headers)
    }

As we can see, this method lets us use the HTTP headers we sent in our first request.

We can simply add all of them, just like above, or only specific ones. Either way- please be cautious with this approach to not leak users’ tokens.

10. Print Redirect Request Info

But what if we don’t necessarily want to access the headers and simply want to log the redirect request information, or add another header?

Well, in such a case I would recommend the HttpClient followRedirect(boolean, Consumer<HttpClientRequest>):

@Bean
fun httpClient(): HttpClient =
  HttpClient.create()
    .followRedirect(true) { redirectRequest ->
      println("URI: ${redirectRequest.uri()}")
      println("Is follow redirect: ${redirectRequest.isFollowRedirect}")
      println("Redirected from: ${redirectRequest.redirectedFrom().firstOrNull()}")
      println("Resource URL: ${redirectRequest.resourceUrl()}")
      redirectRequest.addHeader("another-header", "another-value")
      println("Request headers: ${redirectRequest.requestHeaders()}")
    }

This variant allows us to pass a HttpClientRequest Consumer. As we can see above the instance of HttpClientRequest contains useful info, like:

  • redirect URI,
  • whether the request is following the redirect,
  • previous redirections array (as there might be multiple),
  • the URL we are going to query now,
  • all headers of the upcoming request.

Additionally, with this one, we can set more headers- just like above.

If we run the test now, we should see something like that:

URI: /one-redirect-target
Is follow redirect: true
Redirected from: http://localhost:8090/some-endpoint
Resource URL: http://localhost:8091/one-redirect-target
Request headers: DefaultHttpHeaders[user-agent: ReactorNetty/1.0.23, host: localhost:8091, accept: */*, custom-header: custom-header-value, another-header: another-value]

11. Follow Redirect Only When Conditions Are Met

Lastly, let’s see what can we do to add custom logic checking whether the request should be followed, or not.

As I’ve mentioned earlier, the default implementation enables auto-redirect for the following status codes: 301|302|303|307|308.

If we would like to change this behavior, then the HttpClient followRedirect(BiPredicate<HttpClientRequest, HttpClientResponse>) implementation is a great choice:

@Bean
fun httpClient(): HttpClient =
  HttpClient.create()
    .followRedirect { clientRequest, clientResponse ->
      clientRequest.requestHeaders().contains("custom-header")
        && clientResponse.status().code() == 301
    }

As we can see, the request we will follow the redirect only when our request contains a custom-header header and the response status code is 301.

12. Follow Redirects With Spring WebClient

And that would be all for this article on how to follow redirects (3xx) responses with Spring WebClient.

Together, we learned how to implement such functionality and make use of different implementations shipped with Spring. Please keep in mind that there are two more methods, which I haven’t covered because they basically combine the logic I have shown (but still, they may suit your use-case better).

Finally, if you think this content is useful, or I helped you, then please leave a comment– that helps me to reach an even bigger audience. And as always- you can find the GitHub repository with the source code right here.

Leave a Reply

Your email address will not be published. Required fields are marked *

Categories

Author

Hi there! 👋

Hi there! 👋

My name is Piotr and I've created Codersee to share my knowledge about Kotlin, Spring Framework, and other related topics through practical, step-by-step guides. Always eager to chat and exchange knowledge.

Join the FREE weekly newsletter and get two free eBooks:

Image shows the covers of free ebooks accessible for newsletter subscribers.

You may opt out any time. Terms of Use and Privacy Policy.

To make Codersee work, we log user data. By using our site, you agree to our Privacy Policy and Terms of Use.