From my spring boot app I am making multiple calls to an API search service using the Spring Web Client. This is required due to pagination and multiple search params that cannot be used together.
When making calls with certain params I am getting HTTP 204 No Content, which is completely normal and expected. However this is causing an issue with decoding the body to my Response object
I am attempting to handle the 204 status in a filter but what I am doing seems a bit wonky and wondering how this should be handled. I am new to the reactive style but want to avoid using the deprecated RestTemplate style.
.builder()
.filter(WebClientFilter.handleError())
.filter(responseFilter)
.clientConnector(new ReactorClientHttpConnector(HttpClient.create().followRedirect(true)))
... default header stuff ommitted ...
.build().post().uri(searchServiceUrl)
.body(BodyInserters.fromValue(createsearchRequest()))
.retrieve()
.bodyToMono(SearchResponse.class)
.block();
Here is where I am filtering for the 204 and returning a newly constructed Response with my empty Dto. This just seems wrong that I am replacing the response from the server with my own, but if I do not do this the WebClient returns null causing other issues.
private static Mono<ClientResponse> exchangeFilterResponseProcessor(ClientResponse response) {
HttpStatus status = response.statusCode();
if (HttpStatus.NO_CONTENT.equals(status)) {
return response.bodyToMono(String.class).flatMap(body -> {
log.info("Body is {}" , body);
ClientResponse emptyResponse = ClientResponse.create(HttpStatus.OK)
.header(CONTENT_TYPE, "application/json")
.body(new SearchResponse().toString())
.build();
return Mono.just(emptyResponse);
});
}
return Mono.just(response);
}
Should I refactor the code to just allow the null response and deal with it that way vs trying to do it in the code above?
Related
I am writing an blocking web client. I want to return both body(class) and http status code in ResponseEntity object to be used in some method. Can you please help with this. I am new to Java and already tried approaches many mentioned on internet.
You can use ResponseEntity class!
For e.g. Assume you want to return your Model/Pojo as below
Model class -> MyResponse (Has fields, getter/setters,toString etc) you want to respond along with HTTP code
Code snippet
#GetMapping("/getresponse) //Or any mapping
public ResponseEntity getResponse()
{
MyResponse response=new MyResponse();
return new ResponseEntity>(response,HttpStatus.OK);
}
I'm a bit new to reactive programming, and I'm trying to assemble the following: using Java, Springboot 2, Webflux, and reactor core, I want to handle very specific requests that need extra authentication. So I'm implementing a WebFilter with a series of steps:
Capture the path and the method of the request. Check if the combination exists and needs specific authentication with the accessPointService.getAccessPointAuthorizationRequirement method (returns a Mono with a Boolean).
Since I have CSRF and Spring security configured, I need both csrf token and springsession credentials. I make a GET and a POST request for the credentials.
Then with the credentials, I simply make a POST request to a service (authcheck) that can do a series of security checks (the service is OK, works fine from Postman and Angular).
After that, I need to retrieve the body, convert it to String, and inspect it. Right now this does not happen.
The filter
#Override
public Mono<Void> filter(final ServerWebExchange serverWebExchange, final WebFilterChain webFilterChain) {
//client for specific requests.
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:8080")
.build();
//get request for the CSRF cookie.
WebClient.RequestHeadersSpec<?> getRequest = webClient.get()
.uri("/login");
//post request for the spring security session cookie.
WebClient.RequestHeadersSpec<?> postRequest = webClient.post()
.uri("/login")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.body(BodyInserters.fromFormData("username", "username")
.with("password", "password"));
//services that checks if the given request needs extra authentication
return accessPointService.getAccessPointAuthorizationRequirement(serverWebExchange.getRequest().getMethod().toString().toUpperCase(), serverWebExchange.getRequest().getPath().toString())
.log()
//gets the csrf token from the GET request
.flatMap(isRequired -> getRequest.exchangeToMono(response -> Mono.just(response.cookies().getFirst("XSRF-TOKEN").getValue())))
//combines the previous token with the POST request SESSION cookie,
//THEN secures the last request with both credentials
.zipWith(postRequest.exchangeToMono(resp -> Mono.just(resp.cookies().getFirst("SESSION").getValue())),
AuthenticationFilter::secureAuthRequest)
//gets the exchange from the request and converts the body into a String
.flatMap(AuthenticationFilter::getRequestExchange)
//code to validate if it's doing something. Not implemented yet because it never executes.
.flatMap(s -> Mono.just(s.equals("")))
.onErrorResume(e -> {
throw (CustomException) e;//breaks the execution
})
.then(webFilterChain.filter(serverWebExchange));//continues the execution
}
The secureAuthRequest and getRequestExchange methods invoked
//adds the springsession cookie and csrf cookie to the request
private static WebClient.RequestHeadersSpec<?> secureAuthRequest(String csrf, String spring) {
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:8080")
.build();
WebClient.RequestHeadersSpec<?> request = webClient.post()
.uri("/authcheck")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
request.header("X-XSRF-TOKEN", csrf);
request.cookies( cookies -> cookies.add( "XSRF-TOKEN", csrf) );
request.header("Authorization", spring);
return request;
}
//gets the body as string.
private static Mono<String> getRequestExchange(WebClient.RequestHeadersSpec<?> securedReq) {
return securedReq.exchangeToMono(clientResponse -> clientResponse.bodyToMono(String.class));
}
However, when a request is bound to be authenticated, the log is the following:
2021-10-26 23:57:18.760 INFO 6860 --- [ctor-http-nio-4] reactor.Mono.Just.4 : | onSubscribe([Synchronous Fuseable] Operators.ScalarSubscription)
2021-10-26 23:57:18.761 INFO 6860 --- [ctor-http-nio-4] reactor.Mono.Just.4 : | request(unbounded)
2021-10-26 23:57:18.761 INFO 6860 --- [ctor-http-nio-4] reactor.Mono.Just.4 : | onNext(true)
2021-10-26 23:57:18.762 INFO 6860 --- [ctor-http-nio-4] reactor.Mono.Just.4 : | onComplete()
As far as I know, the stream of data starts with a subscription and a posterior request (which I think returns a TRUE from the accessPointService.getAccessPointAuthorizationRequirement method Mono value, if I'm wrong please correct me), but then the 'onComplete()' log shows up. I don't know exactly what the onComplete() log means, since it's being shown before the execution of the getRequestExchange method (which is invoked). The Mono.just(s.equals("")) piece of code never executes.
I've read a lot about how 'nothing happens until you subscribe', but I still don't know why the reactive flow is being invoked at all if I never explicitly subscribe to the stream, and neither I know how to implement it, since it only returns a Disposable (I guess I can throw exceptions from within?). Also, I hear about decoupling when multiple subscribers are being invoked, so I tried to avoid them as possible.
Any help regarding reactive programming, reactor-core, or the specific flow and how to improve it it's appreciated.
Cheers.
So after some research and thanks to #Toerktumlare 's comments, and figured what was happening and what I changed/applied to this.
So for the 'onComplete()' log, it marks the end of a producer of data. So to see the full stack of the operation, I needed to chain each producer with its own log. For example:
Mono.just(Boolean.FALSE)
.log()
.flatMap(booleanVal -> Mono.just(booleanVal.toString()))
.log()
.subscribe(stringVal -> System.out.println("This is the boolean value " + stringVal));
That will produce the trace for the initial producer and the flatMap operation.
Now, onto the main problem, the issue was within the getRequestExchange method:
//gets the body as string.
private static Mono<String> getRequestExchange(WebClient.RequestHeadersSpec<?> securedReq) {
return securedReq.exchangeToMono(clientResponse -> clientResponse.bodyToMono(String.class));
}
The problem was hidden in the bodyToMono method. According to this site https://medium.com/#jeevjyotsinghchhabda/dont-let-webclient-s-bodytomono-trick-you-645123b3e0a9 , if the response to this request has no body for whatever reason, will not throw any error, but just return a Mono.empty(). Since that the flow was not prepared for such a producer, it ended right there.
In my case, the problem was spring cloud security. I provided the Authorization credential, but not the associated SESSION cookie in the request. So the request returned a 302 (Found) without body. That was the problem (not the reactive flow itself).
So, after that, I modified the request, and #Toerktumlare 's comments helped me develop a working solution:
//service that returns if certain resource needs authentication or not, or if it's not even configured
return accessPointService.getAccessPointAuthorizationRequirement(serverWebExchange.getRequest().getMethod().toString().toUpperCase(), FWKUtils.translateAccessPointPath(serverWebExchange.getRequest().getPath().pathWithinApplication().elements()))
//if the response is a Mono Empty, then returns a not acceptable exception
.switchIfEmpty(Mono.defer(() -> throwNotAcceptable(serverWebExchange)))
//takes the boolean value to check if extra auth is needed.
.flatMap(isRequired -> validateAuthenticationRequirement(isRequired))
//gets the access token - the extra auth credential
.flatMap(isRequired -> getHeaderToken(serverWebExchange))
//from this access generates a WebClient to the specific authentication service - from a webClientProvider to not create too many WebClients.
.flatMap(accessToken -> generateAuthenticationRequest(webClientProvider.getInstance(), accessToken))
//gets the CRSF token credential and secures the request (adds it to the header and the cookies)
.zipWith(getCredential(webClientProvider.getInstance(), "csrf"), (securedRequest, csrfToken) -> secureAuthenticationRequest(securedRequest, csrfToken, "X-XSRF-TOKEN", "XSRF-TOKEN"))
//gets the SESSION (spring cloud security) token credential and secures the request (adds it to the header and the cookies)
.zipWith(getCredential(webClientProvider.getInstance(), "spring-cloud"), (securedRequest, sessionToken) -> secureAuthenticationRequest(securedRequest, sessionToken, "Authorization", "SESSION"))
//does the request and gets the response
.map(requestBodySpecs -> requestBodySpecs.retrieve())
//from the response, maps it to a specific DTO. The single() clause is to validate that a body is present.
.flatMap(clientResponse -> clientResponse.bodyToMono(SecurityCredentialResponseDTO.class).single())
//checks the authentication and throws a Unauthorizedstatus if its not valid.
.flatMap(responseDTO -> checkTokenAuthentication(serverWebExchange, responseDTO))
//if an error is present, then throws it
.onErrorResume(e -> {
if (e instanceof FWKException.GenericException) {
throw (FWKException.GenericException) e;
}
throw (RuntimeException) e;
})
//finally, continues the execution if no exception was thrown.
.then(webFilterChain.filter(serverWebExchange));
There's a bit more that I implemented in this solution (storing the CSRF and spring-cloud credential to avoid innecesary calls).
I'm trying to create a ClientResponse in test and use it for testing a service, which also does deserialization with standard way response.bodyToMono(..class..). But it appears that there is something wrong in the way I build a fake client response. Because I receive UnsupportedMediaTypeException in tests.
Nevertheless the same code work fine in runtime SpringBoot app, when WebClient returns ClientResponse (which is built internally).
Let's see at the simplest case hich fails with
org.springframework.web.reactive.function.UnsupportedMediaTypeException:
Content type 'application/json' not supported for bodyType=java.lang.String[]
void test()
{
String body = "[\"a\", \"b\"]";
ClientResponse response = ClientResponse.create(HttpStatus.OK)
.header(HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_JSON_VALUE)
.body(body)
.build();
String[] array = response.bodyToMono(String[].class).block();
assertEquals(2, array.length);
}
Please, help me to undeerstand, how the client response should be build to allow a standard (json -> object) deserialization in test environment.
A ClientResponse created manually does not have access to Jackson2Json exchange strategies in default list. Probably it could be configured with Spring auto-configuration, which is turned off in tests without Spring context.
Here is the straightforward way to force (de)serialization String <-> json:
static ExchangeStrategies jacksonStrategies()
{
return ExchangeStrategies
.builder()
.codecs(clientDefaultCodecsConfigurer ->
{
clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(new ObjectMapper(), MediaType.APPLICATION_JSON));
clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(new ObjectMapper(), MediaType.APPLICATION_JSON));
}).build();
}
Then use it in the create function
ClientResponse.create(HttpStatus.OK, jacksonStrategies())...
I have a Reactive Spring Application using WebFlux with a REST API. Whenever a user calls my API, I need to make a call to a SOAP service which exposes a WSDL, perform some operation and return the result.
How do I combine this call to a SOAP service with the Reactive WebFlux framework?
The way I see it, I can do it 2 different ways:
Construct and send the SOAP message using WebFlux' WebClient.
Wrapping a synchronous call using WebServiceGatewaySupport in a Mono / Flux.
The first approach has my preference, but I don't know how to do that.
Similar questions have been asked here:
Reactive Spring WebClient - Making a SOAP call, which refers to this blog post (https://blog.godatadriven.com/jaxws-reactive-client). But I could not get that example to work.
Using wsdl2java in a Gradle plugin I can create a client interface with asynchronous methods, but I don't understand how to use this. When using the WebServiceGatewaySupport I don't use that generated interface or its methods at all. Instead, I call the generic marshalSendAndReceive method
public class MySoapClient extends WebServiceGatewaySupport {
public QueryResponse execute() {
Query query = new ObjectFactory().createQuery();
// Further create and set the domain object here from the wsdl2java generated classes
return (QueryResponse) getWebServiceTemplate().marshalSendAndReceive(query);
}
}
Can anyone share a complete example going from a WebFlux controller to making a SOAP call and returning asynchronously? I feel like I am missing something crucial.
I had the same aim but without having WSDL file. As an input I had endpoint and XSD file that defines request's scheme that I should to send. Here is my piece of code.
First let's define our SOPA WebClient bean (to avoid creating it each time when we want to make a call)
#Bean(name = "soapWebClient")
public WebClient soapWebClient(WebClient.Builder webClientBuilder) {
String endpoint = environment.getRequiredProperty(ENDPOINT);
log.info("Initializing SOAP Web Client ({}) bean...", endpoint);
return webClientBuilder.baseUrl(endpoint)
.defaultHeader(CONTENT_TYPE, "application/soap+xml")
//if you have any time limitation put them here
.clientConnector(getWebClientConnector(SOAP_WEBCLIENT_CONNECT_TIMEOUT_SECONDS, SOAP_WEBCLIENT_IO_TIMEOUT_SECONDS))
//if you have any request/response size limitation put them here as well
.exchangeStrategies(ExchangeStrategies.builder()
.codecs(configurer -> configurer.defaultCodecs()
.maxInMemorySize(MAX_DATA_BUFFER_SIZE))
.build())
.build();
}
public static ReactorClientHttpConnector getWebClientConnector(int connectTimeoutSeconds, int ioTimeoutSeconds) {
TcpClient tcpClient = TcpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeoutSeconds * 1000)
.doOnConnected(conn -> conn.addHandlerLast(new ReadTimeoutHandler(ioTimeoutSeconds))
.addHandlerLast(new WriteTimeoutHandler(ioTimeoutSeconds)));
return new ReactorClientHttpConnector(HttpClient.from(tcpClient));
}
And now you can use the client to make SOAP calls like this:
#Slf4j
#Component
public class SOAPClient {
private final WebClient soapWebClient;
public SOAPClient(#Qualifier("soapWebClient") WebClient soapWebClient) {
this.soapWebClient = soapWebClient;
}
public Mono<Tuple2<HttpStatus, String>> send(String soapXML) {
return Mono.just("Request:\n" + soapXML)
.doOnNext(log::info)
.flatMap(xml -> soapWebClient.post()
.bodyValue(soapXML)
.exchange()
.doOnNext(res -> log.info("response status code: [{}]", res.statusCode()))
.flatMap(res -> res.bodyToMono(String.class)
.doOnNext(body -> log.info("Response body:\n{}", body))
.map(b -> Tuples.of(res.statusCode(), b))
.defaultIfEmpty(Tuples.of(res.statusCode(), "There is no data in the response"))))
.onErrorResume(ConnectException.class, e -> Mono.just(Tuples.of(SERVICE_UNAVAILABLE, "Failed to connect to server"))
.doOnEach(logNext(t2 -> log.warn(t2.toString()))))
.onErrorResume(TimeoutException.class, e -> Mono.just(Tuples.of(GATEWAY_TIMEOUT, "There is no response from the server"))
.doOnEach(logNext(t2 -> log.warn(t2.toString()))));
}
}
An important thing to mention here is that your soapXML should be in the format that defined by SOAP protocol obviously. To be more specific the message at least should starts and ends with soap:Envelope tag and consist all other data inside. Also, pay attention what version of SOAP protocol you are about to use as it defines what tags are allowed to use within the envelop and what not. Mine was 1.1 and here is specification for it
https://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383494
cheers
After lots of pain and trouble I found a decent solution to this problem. Since a wsdl file is provided, you should visit this site: : https://www.wsdl-analyzer.com
you can input a wsdl file and view all operations of the soap service. once you find the desired operation you want to call, click on it, and it will show an example request in xml. Some how, you have to generate this xml to make the request. There are many methods to do so, and some are more complicated than others. I found that manual serialization works well, and is honestly easier than using libraries.
say you have an operation request like this:
<s11:Envelope>
<s11:body>
<s11:operation>
<ns:username>username</ns:username>
<ns:password>password</ns:password>
</sll:operation>
</s11:body>
<s11:Envelope>
then you would generate by
public String gePayload(String username, String password) {
StringBuilder payload = new Stringbuilder();
payload.append("<s11:Envelope><s11:body><s11:operation>");
payload.append("<ns:username>");
payload.append(username);
payload.append("</ns:username>");
payload.append("<ns:password>");
payload.append(password);
payload.append("</ns:password>");
payload.append("</s11:operation></s11:body></s11:Envelope>");
return payload.toString()
}
then the web calls
public String callSoap(string payload) {
Webclient webclient = Webclient.builder()
// make sure the path is absolute
.baseUrl(yourEndPoint)
.build()
return WebClient.post()
.contentType(MediaType.TEXT_XML)
.bodyValue(payload)
.retrieve()
.bodyToMono(String.class)
.block();
}
it is important that you specify the content type is xml, and that the class returns a string. web flux cannot easily convert xml to user defined classes. so you do have to preform manual parsing. You can specify jaxb2xmlEncoders and jaxb2xmlDecoders to endcode/decode a specific class, but I found this to be to complicated. the payload has to match the request format generated by wsdl analyzer, and getting the encoders/decoders to match that format can be a task of its own. you can further research these encoders if you want, but this method will work.
I'm facing the same problem for a week and still can't find the best solution.
If you want to test the WebClient you just need to post a string with the SOAP Envelope request. Something like that:
String _request = "<soap:Envelope xmlns:soap=\"http://www.w3.org/2003/05/soap-envelope\">\n" +
"<soap:Body>\n" +
"<request>\n" +
"<Example>blabla</Example>\n" +
"</request>\n" +
"</soap:Body>\n" +
"</soap:Envelope>";
WebClient webClient = WebClient.builder().baseUrl("http://example-service").build();
Mono<String> stringMono = webClient.post()
.uri("/example-port")
.body(BodyInserters.fromObject(_request))
.retrieve()
.bodyToMono(String.class);
stringMono.subscribe(System.out::println);
The problem is that you need to figure out how to serialize the whole SOAP Envelope (request and response) to a string.
This is only an example - not a solution.
In Spring Boot 1.5.x, I could use interceptors with AsyncRestTemplate to grab headers from an incoming request to a RestController endpoint and put them in any exchange requests made via the AsyncRestTemplate.
I don't see how this can work with the WebClient. It looks like if you build a WebClient that all its headers, etc are set and unchangeable:
WebClient client = WebClient.builder()
.baseUrl( "http://blah.com" )
.defaultHeader( "Authorization", "Bearer ey..." )
.build();
While I can change these using client.mutate(), that instantiates a completely new WebClient object. I'd prefer not to have to create a new one on every request. Is there no way to keep a WebClient and have per-request headers and other parameters?
It seems like a big waste and poor performance to force creating a new object every time.
What you're using here are the default headers that should be sent for all requests sent by this WebClient instance. So this is useful for general purpose headers.
You can of course change the request headers on a per-request basis like this:
Mono<String> result = this.webClient.get()
.uri("/greeting")
.header("Something", "value")
.retrieve().bodyToMono(String.class);
If you wish to have an interceptor-like mechanism to mutate the request before sending it, you can configure the WebClient instance with a filter:
WebClient
.builder()
.filter((request, next) -> {
// you can mutate the request before sending it
ClientRequest newRequest = ClientRequest.from(request)
.header("Something", "value").build();
return next.exchange(newRequest);
})
Please check out the Spring Framework documentation about WebClient.