Spring WebFlux, how can I debug my WebClient POST exchange?

31.5k views Asked by At

I am having trouble understanding what I've done wrong in constructing my WebClient request. I would like to understand what the actual HTTP request looks like. (e.g., dumping the raw request to console)

POST /rest/json/send HTTP/1.1
Host: emailapi.dynect.net
Cache-Control: no-cache
Postman-Token: 93e70432-2566-7627-6e08-e2bcf8d1ffcd
Content-Type: application/x-www-form-urlencoded

apikey=ABC123XYZ&from=example%40example.com&to=customer1%40domain.com&to=customer2%40domain.com&to=customer3%40domain.com&subject=New+Sale+Coming+Friday&bodytext=You+will+love+this+sale.

I am using Spring5's reactive tools to build an API. I have a utility class that will send an email using Dyn's email api. I would like to use The new WebClient class to accomplish this (org.springframework.web.reactive.function.client.WebClient)

The following command has been taken from : https://help.dyn.com/email-rest-methods-api/sending-api/#postsend

curl --request POST "https://emailapi.dynect.net/rest/json/send" --data "apikey=ABC123XYZ&[email protected]&[email protected]&[email protected]&[email protected]&subject=New Sale Coming Friday&bodytext=You will love this sale."

When I make the call in curl with real values, the email sends correctly, so I feel like I am generating my request incorrectly.

My Send Command

public Mono<String> send( DynEmailOptions options )
{
    WebClient webClient = WebClient.create();
    HttpHeaders headers = new HttpHeaders();
    // this line causes unsupported content type exception :(
    // headers.setContentType( MediaType.APPLICATION_FORM_URLENCODED );
    Mono<String> result = webClient.post()
        .uri( "https://emailapi.dynect.net/rest/json/send" )
        .headers( headers )
        .accept( MediaType.APPLICATION_JSON )
        .body( BodyInserters.fromObject( options ) )
        .exchange()
        .flatMap( clientResponse -> clientResponse.bodyToMono( String.class ) );
    return result;
}

My DynEmailOptions Class

import java.util.Collections;
import java.util.Set;

public class DynEmailOptions
{
    public String getApikey()
    {
        return apiKey_;
    }

    public Set<String> getTo()
    {
        return Collections.unmodifiableSet( to_ );
    }

    public String getFrom()
    {
        return from_;
    }

    public String getSubject()
    {
        return subject_;
    }

    public String getBodytext()
    {
        return bodytext_;
    }

    protected DynEmailOptions(
        String apiKey,
        Set<String> to,
        String from,
        String subject,
        String bodytext
    )
    {
        apiKey_ = apiKey;
        to_ = to;
        from_ = from;
        subject_ = subject;
        bodytext_ = bodytext;
    }

    private Set<String> to_;
    private String from_;
    private String subject_;
    private String bodytext_;
    private String apiKey_;
}
2

There are 2 answers

3
Brian Clozel On BEST ANSWER

You're currently trying to serialize the request body "as is", without using the right BodyInserter.

In this case, I think you should turn your DynEmailOptions object into a MultiValueMap<String, String> and then:

MultiValueMap<String, String> formData = ...
Mono<String> result = webClient.post()
                .uri( "https://emailapi.dynect.net/rest/json/send" )
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .accept( MediaType.APPLICATION_JSON )
                .body( BodyInserters.fromFormData(formData))
                .retrieve().bodyToMono(String.class);
1
Peter YuChen waNgamer On

The question is about debugging WebClient POST. I've found great help in callicoder.com.

The key is to add a filter in the WebClient. The filter allows easy access to both requests and responses. For both requests and responses, you can access method, URL, headers and other things. However, you can't access the body. I hope I'm wrong, but really, there is only a body() method to SET the body.

Here I have to complain about the weird behavior of WebClient POST. Sometimes, instead of getting a 4XX response immediately, it blocks forever. Sometimes, it gives a 501 response. My advice is that try to use a LinkedMultiValueMap to carry the body, avoid using plain String or java.util.Map.

Here is my sample code, using GitHub V3 API as example:

@Bean
public WebClient client() {
    return WebClient.builder()
        .baseUrl("https://api.github.com")
        .defaultHeader("User-Agent", "Spring-boot WebClient")
        .filter(ExchangeFilterFunctions.basicAuthentication("YOUR_GITHUB_USERNAME", "YOUR_GITHUB_TOKEN"))
        .filter(printlnFilter).build();
}
ExchangeFilterFunction printlnFilter= new ExchangeFilterFunction() {
    @Override
    public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
        System.out.println("\n\n" + request.method().toString().toUpperCase() + ":\n\nURL:"
                + request.url().toString() + ":\n\nHeaders:" + request.headers().toString() + "\n\nAttributes:"
                + request.attributes() + "\n\n");

        return next.exchange(request);
    }
};
//In some method:
String returnedJSON = client.post().uri(builder->builder.path("/user/repos").build())
                .contentType(MediaType.APPLICATION_JSON)
                .syncBody(new LinkedMultiValueMap<String, String>(){{
                    put("name", "tett");
                }})
                .retrieve()
                .bodyToMono(String.class)
                .block(Duration.ofSeconds(3))

You will see things like:

2018-04-07 12:15:57.823  INFO 15448 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port(s): 8084
2018-04-07 12:15:57.828  INFO 15448 --- [           main] c.e.w.WebclientDemoApplication           : Started WebclientDemoApplication in 3.892 seconds (JVM running for 8.426)


POST:

URL:https://api.github.com/user/repos:

Headers:{Content-Type=[application/json], User-Agent=[Spring-boot WebClient], Authorization=[Basic XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX]}

Attributes:{}

There are 2 things to mind about: 1. The sequence of filters matters. Swap these 2 filters and Authentication header will not be included.
2. The filters actually applies to all requests through this WebClient instance.

https://www.callicoder.com/spring-5-reactive-webclient-webtestclient-examples/ is every helpful, maybe you should read it and download his sample code.