How to return bad request in spring webflux when there is an error? - spring-webflux

I have this server endpoint using spring-webflux and I would like to return ServerResponse.badRequest() when the serverRequest receives a wrong parameter. The request curl -s "http://localhost:8080/4?a=5&b=3"; echo for instance, contains the right parameters. But the request curl -s "http://localhost:8080/one?a=5&b=3"; echo contains a string instead of an Integer. Then the conversion new BidRequest(Integer.parseInt(tuple2.getT1()), tuple2.getT2().toSingleValueMap()) will throw an error.
I was doing .onErrorReturn(new BidRequest(0, null)) but now I want to implement some operation that return ServerResponse.badRequest(). So I added in the end .onErrorResume(error -> ServerResponse.badRequest().build()) in the end, but It is not working. I also added on the place of the code .onErrorReturn() and it does not compile.
public Mono<ServerResponse> bidRequest(ServerRequest serverRequest) {
var adId = serverRequest.pathVariable("id");
var attributes = serverRequest.queryParams();
log.info("received bid request with adID: {} attributes: {}", adId, attributes);
return Mono.just(Tuples.of(adId, attributes))
.map(tuple2 -> new BidRequest(Integer.parseInt(tuple2.getT1()), tuple2.getT2().toSingleValueMap()))
// I WANT TO REPLACE IT FOR A BAD REQUEST
// .onErrorReturn(new BidRequest(0, null))
.flatMap(bidRequest -> {
return Flux.fromStream(bidderService.bidResponseStream(bidRequest))
.flatMap(this::gatherResponses)
.reduce((bidResp1, bidResp2) -> {
if (bidResp1.getBid() > bidResp2.getBid()) return bidResp1;
else return bidResp2;
});
})
.map(bid -> {
var price = bid.getContent().replace("$price$", bid.getBid().toString());
bid.setContent(price);
return bid;
})
.flatMap(winner -> {
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(winner.getContent()));
})
.switchIfEmpty(ServerResponse.notFound().build())
// THIS DOES NOT RETURN ANY BAD REQUEST
.onErrorResume(error -> ServerResponse.badRequest().build());
}

I solved based on this answer using flatmap and returning a Mono.just() or a Mono.error(new ResponseStatusException(HttpStatus.BAD_REQUEST));.
return Mono
.just(Tuples.of(adId, attributes))
.flatMap(tuple2 -> {
if (validate(tuple2)) {
log.info("request parameters valid: {}", tuple2);
return Mono.just(new BidRequest(Integer.parseInt(tuple2.getT1()), tuple2.getT2().toSingleValueMap()));
} else {
log.error("request parameters invalid: {}", tuple2);
return Mono.error(new ResponseStatusException(HttpStatus.BAD_REQUEST));
}
})
.flatMap(....
private boolean validate(Tuple2<String, MultiValueMap<String, String>> tuple2) {
return GenericValidator.isInteger(tuple2.getT1());
}

Related

Java reactor and variable scope

I am trying to get my head around variable propagation using reactor. I have a function as follow where I am trying to pass a variable named requestName from outside the map as follow:
public Mono<ResponseEntity<? extends Object>> myFunction(
final Object request, final String requestName) {
return this.client
.create(request)
.exchangeToMono(
response -> {
final HttpStatus status = response.statusCode();
return response
.bodyToMono(String.class)
.defaultIfEmpty(StringUtils.EMPTY)
.map(
body -> {
if (status.is2xxSuccessful()) {
log.info("{}", requestName);
return ResponseEntity.ok().build();
} else {
return ResponseEntity.badRequest().body(null);
}
});
})
.onErrorResume(ex -> Mono.just(buildErrorFromException(requestName, ex)));
}
or another example would be:
String myvar = "test"
return this.
.login()
.flatMap(
response ->
this.myservice(myvar))
.flatMap(
response2 ->
this.myservice2(myvar))
Is it appropriate ? Or would i need to wrap this function around a Mono.deferContextual and apply a contextView ?
Thanks a lot for your help.

How to return a Flux in async/reactive webclient request with subscribe method

I am using spring hexagonal architecture (port and adapter) as my application need to read the stream of data from the source topic, process/transforms the data, and send it to destination topic.
My application need to do the following actions.
Read the data (which will have the call back url)
Make an http call with the url in the incoming data (using webclient)
Get the a actual data and it needs to be transformed into another format.
Send the transformed data to the outgoing topic.
Here is my code,
public Flux<TargeData> getData(Flux<Message<EventInput>> message)
{
return message
.flatMap(it -> {
Event event = objectMapper.convertValue(it.getPayload(), Event.class);
String eventType = event.getHeader().getEventType();
String callBackURL = "";
if (DISTRIBUTOR.equals(eventType)) {
callBackURL = event.getHeader().getCallbackEnpoint();
WebClient client = WebClient.create();
Flux<NodeInput> nodeInputFlux = client.get()
.uri(callBackURL)
.headers(httpHeaders -> {
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
List<MediaType> acceptTypes = new ArrayList<>();
acceptTypes.add(MediaType.APPLICATION_JSON);
httpHeaders.setAccept(acceptTypes);
})
.exchangeToFlux(response -> {
if (response.statusCode()
.equals(HttpStatus.OK)) {
System.out.println("Response is OK");
return response.bodyToFlux(NodeInput.class);
}
return Flux.empty();
});
nodeInputFlux.subscribe( nodeInput -> {
SourceData source = objectMapper.convertValue(nodeInput, SourceData.class);
// return Flux.fromIterable(this.TransformImpl.transform(source));
});
}
return Flux.empty();
});
}
The commented line in the above code is giving the compilation as subscribe method does not allow return types.
I need a solution "without using block" here.
Please help me here, Thanks in advance.
I think i understood the logic. What do you may want is this:
public Flux<TargeData> getData(Flux<Message<EventInput>> message) {
return message
.flatMap(it -> {
// 1. marshall and unmarshall operations are CPU expensive and could harm event loop
return Mono.fromCallable(() -> objectMapper.convertValue(it.getPayload(), Event.class))
.subscribeOn(Schedulers.parallel());
})
.filter(event -> {
// 2. Moving the if-statement yours to a filter - same behavior
String eventType = event.getHeader().getEventType();
return DISTRIBUTOR.equals(eventType);
})
// Here is the trick 1 - your request below return Flux of SourceData the we will flatten
// into a single Flux<SourceData> instead of Flux<List<SourceData>> with flatMapMany
.flatMap(event -> {
// This WebClient should not be created here. Should be a singleton injected on your class
WebClient client = WebClient.create();
return client.get()
.uri(event.getHeader().getCallbackEnpoint())
.accept(MediaType.APPLICATION_JSON)
.exchangeToFlux(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
System.out.println("Response is OK");
return response.bodyToFlux(SourceData.class);
}
return Flux.empty();
});
})
// Here is the trick 2 - supposing that transform return a Iterable of TargetData, then you should do this and will have Flux<TargetData>
// and flatten instead of Flux<List<TargetData>>
.flatMapIterable(source -> this.TransformImpl.transform(source));
}

Handling Global Scenarios in Spring WebFlux

I have a Rest Web Client todo an API call and I handle the exceptions as given below.
I want to handle 404, 401 and 400 errors in a global way rather than handling at the individual client level. How can we achieve the same.
public Mono<ProductResponse> getProductInformation(String productId) {
return webClient.get()
.uri("/v1/products/"+productId)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus( httpStatus -> HttpStatus.NOT_FOUND.equals(httpStatus), clientResponse -> {
Mono<NotFound> notFound = clientResponse.bodyToMono(NotFound.class);
return notFound.flatMap( msg -> {
log.info(" Error Message {}" , msg.getErrorMsg());
return Mono.error(new NotFoundException(msg.getErrorMsg()));
});
}).onStatus( httpStatus -> HttpStatus.UNAUTHORIZED.equals(httpStatus), clientResponse -> {
Mono<NotFound> notFound = clientResponse.bodyToMono(NotFound.class);
return Mono.error(new NotAuthorisedException("Unauthorised"));
}).bodyToMono(ProductResponse.class);
}
Two approaches:
Exceptions with webclients are all wrapped in WebClientResponseException class. You can handle that using Spring's ExceptionHandler annotation like this.
#ExceptionHandler(WebClientResponseException.class)
public ResponseEntity handleWebClientException(WebClientResponseException ex){
return ResponseEntity.badRequest().body(ex.getResponseBodyAsString());
}
Note - Here you can write complex conditional logic based on the response status, by using methods like getStatusCode(), getRawStatusCode(), getStatusText(), getHeaders() and getResponseBodyAsString(). Also you can get reference of the request that was sent using the method getRequest.
Using ExchangeFilterFunction while constructing the webclient bean.
#Bean
public WebClient buildWebClient() {
Function<ClientResponse, Mono<ClientResponse>> webclientResponseProcessor =
clientResponse -> {
HttpStatus responseStatus = clientResponse.statusCode();
if (responseStatus.is4xxClientError()) {
System.out.println("4xx error");
return Mono.error(new MyCustomClientException());
} else if (responseStatus.is5xxServerError()) {
System.out.println("5xx error");
return Mono.error(new MyCustomClientException());
}
return Mono.just(clientResponse);
};
return WebClient.builder()
.filter(ExchangeFilterFunction.ofResponseProcessor(webclientResponseProcessor)).build();
}
Then you can either handle the MyCustomClientException using #ExceptionHandler or leave it as it is.

What is the best possible way to send custom error responses in .net core web api

I'm making a .net Core WebApi using .Net Core 2.2. The API is ready but the failure message and response is where I'm stuck at.
Right now, I'm getting respose like below
json
{
"empId":1999,
"empName":"Conroy, Deborah",
"enrollmentStatus":true,
"primaryFingerprintScore":65,
"secondaryFingerprintScore":60,
"primaryFingerprint":null,
"secondaryFingerprint":null,
"primaryFingerprintType":null,
"secondaryFingerprintType":null}
}
I created a json formatter class and wrote the below code
public class SuperJsonOutputFormatter : JsonOutputFormatter
{
public SuperJsonOutputFormatter(
JsonSerializerSettings serializerSettings,
ArrayPool<char> charPool) : base(serializerSettings, charPool)
{
}
public override async Task WriteResponseBodyAsync(
OutputFormatterWriteContext context,
Encoding selectedEncoding)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
if (selectedEncoding == null)
throw new ArgumentNullException(nameof(selectedEncoding));
using (TextWriter writer =
context.WriterFactory(
context.HttpContext.Response.Body,
selectedEncoding))
{
var rewrittenValue = new
{
resultCode = context.HttpContext.Response.StatusCode,
resultMessage =
((HttpStatusCode)context.HttpContext.Response.StatusCode)
.ToString(),
result = context.Object
};
this.WriteObject(writer, rewrittenValue);
await writer.FlushAsync();
}
}
I expect all the error codes to be sent as generic error messages like the JSON below.
FOR STATUS OKAY:
{
"status" : True,
"error" : null,
"data" : {
{
"empId":1999,
"empName":"Conroy, Deborah",
"enrollmentStatus":true,
"primaryFingerprintScore":65,
"secondaryFingerprintScore":60,
"primaryFingerprint":null,
"secondaryFingerprint":null,
"primaryFingerprintType":null,
"secondaryFingerprintType":null}
}
}
}
FOR OTHER STATUS LIKE 404, 500, 400, 204
{
"status" : False,
"error" : {
"error code" : 404,
"error description" : Not Found
},
"data" : null
}
I expect all the error codes to be sent as generic error messages like the JSON below
You're almost there. What you need to do is enabling your SuperJsonOutputFormatter.
A Little Change to Your Formatter
Firstly, your formatter didn't return a json with the same schema as you want. So I create a dummy class to hold the information for error code and error description:
public class ErrorDescription{
public ErrorDescription(HttpStatusCode statusCode)
{
this.Code = (int)statusCode;
this.Description = statusCode.ToString();
}
[JsonProperty("error code")]
public int Code {get;set;}
[JsonProperty("error description")]
public string Description {get;set;}
}
And change your WriteResponseBodyAsync() method as below:
...
using (TextWriter writer = context.WriterFactory(context.HttpContext.Response.Body, selectedEncoding)) {
var statusCode = context.HttpContext.Response.StatusCode;
var rewrittenValue = new {
status = IsSucceeded(statusCode),
error = IsSucceeded(statusCode) ? null : new ErrorDescription((HttpStatusCode)statusCode),
data = context.Object,
};
this.WriteObject(writer, rewrittenValue);
await writer.FlushAsync();
}
Here the IsSucceeded(statusCode) is a simple helper method that you can custom as you need:
private bool IsSucceeded(int statusCode){
// I don't think 204 indicates that's an error.
// However, you could comment out it if you like
if(statusCode >= 400 /* || statusCode==204 */ ) { return false; }
return true;
}
Enable your Formatter
Secondly, to enable your custom Formatter, you have two approaches: One way is to register it as an global Formatter, the other way is to enable it for particular Controller or Action. Personally, I believe the 2nd way is better. So I create a Action Filter to enable your formatter.
Here's an implementation of the Filter that enables your custom formatter dynamically:
public class SuperJsonOutputFormatterFilter : IAsyncActionFilter{
private readonly SuperJsonOutputFormatter _formatter;
// inject your SuperJsonOutputFormatter service
public SuperJsonOutputFormatterFilter(SuperJsonOutputFormatter formatter){
this._formatter = formatter;
}
// a helper method that provides an ObjectResult wrapper over the raw object
private ObjectResult WrapObjectResult(ActionExecutedContext context, object obj){
var wrapper = new ObjectResult(obj);
wrapper.Formatters.Add(this._formatter);
context.Result= wrapper;
return wrapper;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
ActionExecutedContext resultContext = await next();
// in case we get a 500
if(resultContext.Exception != null && ! resultContext.ExceptionHandled){
var ewrapper = this.WrapObjectResult(resultContext, new {});
ewrapper.StatusCode = (int) HttpStatusCode.InternalServerError;
resultContext.ExceptionHandled = true;
return;
}
else {
switch(resultContext.Result){
case BadRequestObjectResult b : // 400 with an object
var bwrapper=this.WrapObjectResult(resultContext,b.Value);
bwrapper.StatusCode = b.StatusCode;
break;
case NotFoundObjectResult n : // 404 with an object
var nwrapper=this.WrapObjectResult(resultContext,n.Value);
nwrapper.StatusCode = n.StatusCode;
break;
case ObjectResult o : // plain object
this.WrapObjectResult(resultContext,o.Value);
break;
case JsonResult j : // plain json
this.WrapObjectResult(resultContext,j.Value);
break;
case StatusCodeResult s: // other statusCodeResult(including NotFound,NoContent,...), you might want to custom this case
var swrapper = this.WrapObjectResult(resultContext, new {});
swrapper.StatusCode = s.StatusCode;
break;
}
}
}
}
And don't forget to register your formatter as a service :
services.AddScoped<SuperJsonOutputFormatter>();
Finally, when you want to enable your formatter, just add a [TypeFilter(typeof(SuperJsonOutputFormatterFilter))] annotation for the controller or action.
Demo
Let's create an action method for Test:
[TypeFilter(typeof(SuperJsonOutputFormatterFilter))]
public IActionResult Test(int status)
{
// test json result(200)
if(status == 200){ return Json(new { Id = 1, }); }
// test 400 object result
else if(status == 400){ return BadRequest( new {}); }
// test 404 object result
else if(status == 404){ return NotFound(new { Id = 1, }); }
// test exception
else if(status == 500){ throw new Exception("unexpected exception"); }
// test status code result
else if(status == 204){ return new StatusCodeResult(204); }
// test normal object result(200)
var raw = new ObjectResult(new XModel{
empId=1999,
empName = "Conroy, Deborah",
enrollmentStatus=true,
primaryFingerprintScore=65,
secondaryFingerprintScore=60,
primaryFingerprint = null,
secondaryFingerprint= null,
primaryFingerprintType=null,
secondaryFingerprintType=null
});
return raw;
}
Screenshot:

I am at a loss of how to create an appropriate flow using webflux

Problem statement: I have a POST request to a booking-service API that gets a BookingRecord. I map it to extract the values so as to call another fare-service API using WebClient. I receive a Mono<Fare> from that call. I need to check whether the value of getFare() method of BookingRecord type is same as the getFare() of the Fare type returned by the WebClient. If not, I need to raise and exception, and pass it on to the caller. Here caller is another Microservice, ui-service calling the booking-service API (so how should I deal with this, pass the error back or else what is the best thing to do?) or else I will save the new BookingRecord and return the id of that record to the caller. What is the best flow sequence for this? I tried my best without much success and am pasting the code here.
public HandlerFunction<ServerResponse> book = request ->
{
request.bodyToMono(BookingRecord.class)
.map(br ->
{
this.webClient.get()
.uri("/fares/get/{flightNumber}/{flightDate}",
br.getFlightNumber(),
br.getFlightDate())
.retrieve()
.bodyToMono(Fare.class)
.map(f ->
{
if (!f.getFare()
.equals(br.getFare()))
{
throw new RuntimeException("Fare is tampered");
}
else
{
id = bookingRepository.save(br).getId();
}
return id;
})
.subscribe();
return id;
});
return ServerResponse.ok()
.body(BodyInserters.fromObject(id));
};
After much tweaking, this is what I did. Hope its the right thing to do. 1. I raise a 500 Http error from the fare-service itself instead of checking in the booking-service.
public HandlerFunction<ServerResponse> getFare = request ->
{
String flightNumber = request.pathVariable("flightNumber");
String flightDate = request.pathVariable("flightDate");
String fare = request.pathVariable("fare");
Mono<ServerResponse> notFound = ServerResponse.notFound()
.build();
return Mono
.justOrEmpty(faresRepository.getFareByFlightNumberAndFlightDateAndFare(flightNumber,
flightDate,
fare))
.flatMap(f -> ServerResponse.ok()
.contentType(APPLICATION_JSON)
.body(fromObject(f)))
.switchIfEmpty(notFound);
};
Handled the exception like so in booking-service using onStatus() method
public HandlerFunction<ServerResponse> book = request ->
{
logger.info("Inside Book function");
return request.bodyToMono(BookingRecord.class)
.flatMap(br ->
{
logger.info("Calling fare-service");
return this.webClient.get()
.uri("/fares/get/{flightNumber}/{flightDate}/{fare}",
br.getFlightNumber(),
br.getFlightDate(),
br.getFare())
.retrieve()
.onStatus(HttpStatus::isError,
x -> Mono
.error(new RuntimeException("Fare has been tampered with!!")))
.bodyToMono(Fare.class);
})
.map(fare ->
{
logger.info("Saving a BookingRecord");
BookingRecord br = new BookingRecord();
br.setFlightNumber(fare.getFlightNumber());
br.setFlightDate(fare.getFlightDate());
br.setFare(fare.getFare());
long id = bookingRepository.save(br)
.getId();
return id;
})
.flatMap(id -> ServerResponse.ok()
.body(BodyInserters.fromObject(id)));
};
This way, I get an exception for fare tampering or get the id for a successful db save.