How to write tescase for webclient onstatus method - spring-boot

I am new to spring webclient and i have written a generic which can be used to consume rest apis in my application:
private Function<ClientResponse, Mono<? extends Throwable>> errorStrategy() {
return response -> {
return response.bodyToMono(Errors.class).flatMap(errorResponse -> {
log.info("Track Error ----> {}", errorResponse.getErrorCode());
Errors errors = new Errors(errorResponse.getErrorMsg());
return Mono.error(errors);
});
};
}
public Mono<EnterpriseSearchResponse> getCustomerID(EnterpriseSearchRequest searchRequest) {
Mono<EnterpriseSearchResponse> response = this.client.method(HttpMethod.GET)
.uri(enterpriseSearchURI + enterpriseSearchContext)
.header("Authorization", "Bearer " + enterpriseSearchAuthToken)
.accept(new MediaType[] { MediaType.APPLICATION_JSON }).bodyValue(searchRequest).retrieve()
.onStatus(HttpStatus::is5xxServerError, errorStrategy())
.onStatus(HttpStatus::is4xxClientError, errorStrategy()).bodyToMono(EnterpriseSearchResponse.class);
return response;
}
i wanted to write junit test case for if consumed rest-api return 404 or 500 error.
can someone suggest how to achieve that?

Related

How to send URL encoded data in spring webflux

I am writing a spring 5 web app and my requirement is to get a urlencoded form and in response send url encoded response back
This is Router Function code
#Configuration
public class AppRoute {
#Bean
public RouterFunction<ServerResponse> route(FormHandler formHandler) {
return RouterFunctions.route()
// .GET("/form", formHandler::sampleForm)
// .POST("/form", accept(MediaType.APPLICATION_FORM_URLENCODED), formHandler::displayFormData)
.POST("/formnew", accept(MediaType.APPLICATION_FORM_URLENCODED).and(contentType(MediaType.APPLICATION_FORM_URLENCODED)), formHandler::newForm)
.build();
}
}
and here's my Handler code
public Mono<ServerResponse> newForm(ServerRequest request) {
Mono<MultiValueMap<String, String>> formData = request.formData();
MultiValueMap<String, String> newFormData = new LinkedMultiValueMap<String, String>();
formData.subscribe(p -> newFormData.putAll(p));
newFormData.add("status", "success");
return ServerResponse.ok().contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(fromObject(newFormData));
}
Here's the error I get
2020-04-07 02:37:33.329 DEBUG 38688 --- [ctor-http-nio-3] org.springframework.web.HttpLogging : [07467aa5] Resolved [UnsupportedMediaTypeException: Content type 'application/x-www-form-urlencoded' not supported for bodyType=org.springframework.util.LinkedMultiValueMap] for HTTP POST /formnew
Whats the issue here. I couldn't find any way to write the url encoded response back.
Could anyone point what's the issue.
Try to refactor your code to functional style:
public Mono<ServerResponse> newForm(ServerRequest request) {
Mono<DataBuffer> resultMono = request.formData()
.map(formData -> new LinkedMultiValueMap(formData))
.doOnNext(newFormData -> newFormData.add("status", "success"))
.map(linkedMultiValueMap -> createBody(linkedMultiValueMap));
return ServerResponse.ok().contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(BodyInserters.fromDataBuffers(resultMono));
}
private DataBuffer createBody(MultiValueMap multiValueMap) {
try {
DefaultDataBufferFactory factory = new DefaultDataBufferFactory();
return factory.wrap(ByteBuffer.wrap(objectMapper.writeValueAsString(multiValueMap).getBytes(StandardCharsets.UTF_8)));
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("incorrect body");
}
}

How to handle exceptions thrown by the webclient?

I'm trying to figure out how to log exceptions from the webclient, whatever the error status code that is returned from the api that gets called.
I've seen the following implementation:
.onStatus(status -> status.value() != HttpStatus.OK.value(),
rs -> rs.bodyToMono(String.class).map(body -> new IOException(String.format(
"Response HTTP code is different from 200: %s, body: '%s'", rs.statusCode(), body))))
Another example I've seen uses a filter. I guess this filter could be used to log errors as well, aside from requests like in this example:
public MyClient(WebClient.Builder webClientBuilder) {
webClient = webClientBuilder // you can also just use WebClient.builder()
.baseUrl("https://httpbin.org")
.filter(logRequest()) // here is the magic
.build();
}
But are we serious that there is no dedicated exception handler to this thing?
Found it.
bodyToMono throws a WebClientException if the status code is 4xx (client error) or 5xx (Server error).
Full implementation of the service:
#Service
public class FacebookService {
private static final Logger LOG = LoggerFactory.getLogger(FacebookService.class);
private static final String URL_DEBUG = "https://graph.facebook.com/debug_token";
private WebClient webClient;
public FacebookService() {
webClient = WebClient.builder()
.filter(logRequest())
.build();
}
public Mono<DebugTokenResponse> verifyFbAccessToken(String fbAccessToken, String fbAppToken) {
LOG.info("verifyFacebookToken for " + String.format("fbAccessToken: %s and fbAppToken: %s", fbAccessToken, fbAppToken));
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(URL_DEBUG)
.queryParam("input_token", fbAccessToken)
.queryParam("access_token", fbAppToken);
return this.webClient.get()
.uri(builder.toUriString())
.retrieve()
.bodyToMono(DebugTokenResponse.class);
}
private static ExchangeFilterFunction logRequest() {
return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
LOG.info("Request: {} {}", clientRequest.method(), clientRequest.url());
clientRequest.headers().forEach((name, values) -> values.forEach(value -> LOG.info("{}={}", name, value)));
return Mono.just(clientRequest);
});
}
#ExceptionHandler(WebClientResponseException.class)
public ResponseEntity<String> handleWebClientResponseException(WebClientResponseException ex) {
LOG.error("Error from WebClient - Status {}, Body {}", ex.getRawStatusCode(), ex.getResponseBodyAsString(), ex);
return ResponseEntity.status(ex.getRawStatusCode()).body(ex.getResponseBodyAsString());
}
}

I need to fetch the auth token and set it in the header

I'm new to Spring boot and reactive programming.
I'm using spring webflux webclient for an external api service. I need to fetch the auth token and set it in the header
WebClient.builder()
.baseUrl(baseUrl)
.filter((request, next) -> {
return next.exchange(request)
.flatMap((Function<ClientResponse, Mono<ClientResponse>>) clientResponse -> {
if (clientResponse.statusCode().value() == 401) {
return authenticate().map(token -> {
Token accessToken = authenticate().block();
ClientRequest retryRequest = ClientRequest.from(request).header("Authorisation", "Bearer " + accessToken.getAccessToken()).build();
return next.exchange(retryRequest);
}).
} else {
return Mono.just(clientResponse);
}
});
})
.defaultHeader("Authorization", "Bearer " + authToken.getAccessToken())
.build();
private Mono<Token> authenticate() {
MultiValueMap<String, String> params = new LinkedMultiValueMap();
params.add("client_id", clientId);
params.add("client_secret", clientSecret);
params.add("grant_type", "password");
params.add("username", username);
params.add("password", password);
WebClient client = WebClient.create(baseUrl);
return client
.post()
.uri(tokenUri)
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.syncBody(params)
.retrieve()
.bodyToMono(Token.class);
}
private static class Token {
#JsonProperty("access_token")
private String accessToken;
public String getAccessToken() { return accessToken; }
}
During the application startup, I'll fetch the access token and set it in the webclient builder. I've created a filter to handle authentication failures after token expiry. But the above code throws error because I've used block() which is not supposed to be used in a reactor thread. How else can I handle it? I'm using oauth2 resource owner password grant flow. Is there is any other way to handle the flow?
Hi I had the same issue (Adding a retry all requests of WebClient) which looks like you have reused.
but here flatmap is your friend, if you have a Mono<Mono<T>> you can flatten it with flatMap
builder.baseUrl("http://localhost:8080")
//sets the header before the exchange
.filter(((request, next) -> tokenProvider.getAccessToken()
.map(setBearerTokenInHeader(request))
.flatMap(next::exchange)))
//do the exchange
.filter((request, next) -> next.exchange(request)
.flatMap(clientResponse -> {
if (clientResponse.statusCode().value() == 401) {
//If unauthenicated try again
return authenticate()
.flatMap(Token::getAccessToken)
.map(setBearerTokenInHeader(request))
.flatMap(next::exchange);
} else {
return Mono.just(clientResponse);
}
}))
.build();
private Function<String, ClientRequest> setBearerTokenInHeader(ClientRequest request) {
return token -> ClientRequest.from(request).header("Bearer ", token).build();
}
I know this is an old thread, but I could not find any other working examples for the initial question
Basically, I was not able to write a working code from the above examples...
With the main task: Use WebClient instance to get protected resource by providing Bearer token. The Bearer token can be requested by a separate request.
The Mono authenticate() should work fine to get a new token.
WebClient client2 = WebClient.builder()
.baseUrl(SERVER_URL)
.filter((request, next) -> {
return next.exchange(request)
.flatMap( clientResponse -> {
if (clientResponse.statusCode().value() == 401) {
return authenticate().map(token -> {
Token accessToken = authenticate().block();
ClientRequest retryRequest = ClientRequest.from(request).header("Authorisation", "Bearer " + accessToken.getAccessToken()).build();
return next.exchange(retryRequest);
});
} else {
return Mono.just(clientResponse);
}
});
})
.defaultHeader("Authorization", "Bearer " + token.getAccessToken())
.build();
For the above example was not able to replace the ".block()" with flatMap()
And the second example
WebClient client3 = WebClient.builder().baseUrl("http://localhost:8080")
//sets the header before the exchange
.filter(((request, next) -> tokenProvider.getAccessToken()
.map(setBearerTokenInHeader(request))
.flatMap(next::exchange)))
//do the exchange
.filter((request, next) -> next.exchange(request)
.flatMap(clientResponse -> {
if (clientResponse.statusCode().value() == 401) {
//If unauthenicated try again
return authenticate()
.flatMap(Token::getAccessToken)
.map(setBearerTokenInHeader(request))
.flatMap(next::exchange);
} else {
return Mono.just(clientResponse);
}
}))
.build();
Not sure what is the "tokenProvider.getAccessToken()" and ".flatMap(Token::getAccessToken)" won't accept
Due to
class Token {
String token = "";
public String getAccessToken() { return token; }
}
Sorry I'm new to this. If you had a working example please share in this thread

Spring webflux "Only one connection receive subscriber allowed" if return server response from switchIfEmpty

I would like to put a case where if object exist then send error if not then create new user.
here is my handler:
public Mono<ServerResponse> createUser(ServerRequest request) {
Mono<UserBO> userBOMono = request.bodyToMono(UserBO.class);
Mono<String> email = userBOMono.map(UserBO::getEmail);
Mono<User> userMono = email.flatMap(userRepository::findByEmail);
return userMono.flatMap(user -> {
Mono<ErrorResponse> errorResponseMono = errorHanlder.handleEmailAlreadyExist();
return ServerResponse.status(HttpStatus.CONFLICT)
.contentType(MediaType.APPLICATION_JSON)
.body(errorResponseMono, ErrorResponse.class);
}).switchIfEmpty(Mono.defer(() -> {
Mono<User> newUserMono = userBOMono.flatMap(userMapping::mapUserBOToUser);
Mono<User> dbUserMono = newUserMono.flatMap(userRepository::save);
return ServerResponse.status(HttpStatus.CREATED)
.contentType(MediaType.APPLICATION_JSON)
.body(dbUserMono, User.class);
}));
if Mono is not empty then its return conflict that what I want if if empty then create new but its throwing below error:
java.lang.IllegalStateException: Only one connection receive subscriber allowed.
at reactor.ipc.netty.channel.FluxReceive.startReceiver(FluxReceive.java:276) ~[reactor-netty-0.7.8.RELEASE.jar:0.7.8.RELEASE]
at reactor.ipc.netty.channel.FluxReceive.lambda$subscribe$2(FluxReceive.java:127) ~[reactor-netty-0.7.8.RELEASE.jar:0.7.8.RELEASE]
at io.netty.util.concurrent.AbstractEventExecutor.safeExecute$$$capture(AbstractEventExecutor.java:163) ~[netty-common-4.1.27.Final.jar:4.1.27.Final]
at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java) ~[netty-common-4.1.27.Final.jar:4.1.27.Final]
at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:404) ~[netty-common-4.1.27.Final.jar:4.1.27.Final]
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:464) ~[netty-transport-4.1.27.Final.jar:4.1.27.Final]
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:884) ~[netty-common-4.1.27.Final.jar:4.1.27.Final]
at java.lang.Thread.run(Thread.java:748) ~[na:1.8.0_131]
Update Note: its correct behavior as per method definition:
switchIfEmpty(Mono<? extends T> alternate)
Fallback to an alternative Mono if this mono is completed without data
Means when I am sending empty Mono in body its work fine:
return ServerResponse.status(HttpStatus.CREATED)
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.empty(), User.class);
so what is solution to handle swtichIfEmpty case if I would like to send Mono object as return from it.
Finally I was able to resolve it, I was reading userBOMono stream twice which was causing this error to throw by webflux.
so here is updated code which works fine.
public Mono<ServerResponse> createUser(ServerRequest request) {
Mono<UserBO> userBOMono = request.bodyToMono(UserBO.class);
return userBOMono.flatMap(userBO -> {
String email = userBO.getEmail();
Mono<User> userMono = userRepository.findByEmail(email);
return userMono.flatMap(user -> errorHandler.handleEmailAlreadyExist())
.switchIfEmpty(Mono.defer(() -> createNewUser(userBO)));
});
}
private Mono<ServerResponse> createNewUser(UserBO userBO) {
Mono<User> userMono = Mono.just(userBO).flatMap(userMapping::mapUserBOToUser).flatMap(userRepository::save);
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON)
.body(userMono, User.class);
}
I assume you use a WebClient to invoke this API.
The client should not subscribe more than once, otherwise this error can come.
I've got the same error by running my #SpringBootTest class.
The problem seems to be that response was being writed while methods had already been closed.
Solved by passing "Mono.empty()" instead of full response.
Code Before:
WebClient.create()
.get()
.uri(new URI(UPDATE_COMPANIES_URL))
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(Boolean.class).thenReturn(Boolean.TRUE);
} else {
System.out.println("[sendSecureRequest] Error sending request: " + response.statusCode());
return response.bodyToMono(Boolean.class).thenReturn(Boolean.FALSE);
}
}).subscribe();
Code After:
WebClient.create()
.get()
.uri(new URI(UPDATE_COMPANIES_URL))
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
// TODO handle success
} else {
System.out.println("[sendSecureRequest] Error sending request: " + response.statusCode());
}
return Mono.empty();
}).subscribe();

Get API response error message using Web Client Mono in Spring Boot

I am using webflux Mono (in Spring boot 5) to consume an external API. I am able to get data well when the API response status code is 200, but when the API returns an error I am not able to retrieve the error message from the API. Spring webclient error handler always display the message as
ClientResponse has erroneous status code: 500 Internal Server Error, but when I use PostMan the API returns this JSON response with status code 500.
{
"error": {
"statusCode": 500,
"name": "Error",
"message":"Failed to add object with ID:900 as the object exists",
"stack":"some long message"
}
}
My request using WebClient is as follows
webClient.getWebClient()
.post()
.uri("/api/Card")
.body(BodyInserters.fromObject(cardObject))
.retrieve()
.bodyToMono(String.class)
.doOnSuccess( args -> {
System.out.println(args.toString());
})
.doOnError( e ->{
e.printStackTrace();
System.out.println("Some Error Happend :"+e);
});
My question is, how can I get access to the JSON response when the API returns an Error with status code of 500?
If you want to retrieve the error details:
WebClient webClient = WebClient.builder()
.filter(ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
if (clientResponse.statusCode().isError()) {
return clientResponse.bodyToMono(ErrorDetails.class)
.flatMap(errorDetails -> Mono.error(new CustomClientException(clientResponse.statusCode(), errorDetails)));
}
return Mono.just(clientResponse);
}))
.build();
with
class CustomClientException extends WebClientException {
private final HttpStatus status;
private final ErrorDetails details;
CustomClientException(HttpStatus status, ErrorDetails details) {
super(status.getReasonPhrase());
this.status = status;
this.details = details;
}
public HttpStatus getStatus() {
return status;
}
public ErrorDetails getDetails() {
return details;
}
}
and with the ErrorDetails class mapping the error body
Per-request variant:
webClient.get()
.exchange()
.map(clientResponse -> {
if (clientResponse.statusCode().isError()) {
return clientResponse.bodyToMono(ErrorDetails.class)
.flatMap(errorDetails -> Mono.error(new CustomClientException(clientResponse.statusCode(), errorDetails)));
}
return clientResponse;
})
Just as #Frischling suggested, I changed my request to look as follows
return webClient.getWebClient()
.post()
.uri("/api/Card")
.body(BodyInserters.fromObject(cardObject))
.exchange()
.flatMap(clientResponse -> {
if (clientResponse.statusCode().is5xxServerError()) {
clientResponse.body((clientHttpResponse, context) -> {
return clientHttpResponse.getBody();
});
return clientResponse.bodyToMono(String.class);
}
else
return clientResponse.bodyToMono(String.class);
});
I also noted that there's a couple of status codes from 1xx to 5xx, which is going to make my error handling easier for different cases
Look at .onErrorMap(), that gives you the exception to look at. Since you might also need the body() of the exchange() to look at, don't use retrieve, but
.exchange().flatMap((ClientResponse) response -> ....);

Resources