How can I get webflux response body twice? Store some data from request/response - spring-webflux

I created a filter to log some important data from the request and response...
#Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
URI uri = request.url();
HttpMethod method = request.method();
return next.exchange(request).onErrorResume(err -> {
writeToFile(method, uri, 500, "", true);
return Mono.error(err);
}).flatMap((response) -> {
return response.bodyToMono(String.class).flatMap(body -> {
writeToFile(method, uri, response.rawStatusCode(), body, false);
return Mono.just(response);
});
});
}
The idea of this filter is to register some data and let the rest work exactly the same.
Expected: This was my attempt to keep the rest of the code untouched and just include a filter to store some data apart from the normal flow.
Actual: It works, except when the real code calls bodyToMono, it returns null
At some point of the normal flow, I am doing something similar to this:
return client.get().uri(...).exchangeToMono((response) -> {
return response.bodyToMono(MyObject.class);
})
It works flawlessly when I remove the filter.
So I discovered I can't call bodyToMono twice the way I am using probably because the first call is consuming the body and then it is empty in the second call.

Related

Spring webflux : consume mono or flux from request

I have a resource API that handles an object (Product for example).
I use PUT to update this object in the database.
And I want to return just en empty Mono to the user.
There is my code :
public Mono<ServerResponse> updateProduct(ServerRequest request){
Mono<Product> productReceived = request.bodyToMono(Product.class);
Mono<Product> result = productReceived.flatMap(item -> {
doSomeThing(item);
System.out.println("Called or not called!!");
return Mono.just(productService.product);
}).subscribe();
return ok()
.contentType(APPLICATION_JSON)
.body(Mono.empty(), Product.class);
}
The problem is my method doSomeThing() and the println are not called.
NB: I use subscribe but doesn't work.
Thanks.
I had a similar issue when I was new to Webflux. In short, you can't call subscribe on the request body and asynchronously return a response because the subscription might not have enough time to read the body. You can see a full explanation of a similar issue here.
To make your code work, you should couple the response with your logic stream. It should be something like the following:
public Mono<ServerResponse> updateProduct(ServerRequest request){
return request
.bodyToMono(Product.class)
.flatMap(item -> {
doSomeThing(item);
System.out.println("Called or not called!!");
return Mono.just(productService.product);
})
.then(ServerResponse.ok().build());
}

Cannot redirect another page in ASP.NET Core MVC project Controller

I would like to press a button to connect a GET api, do something and redirect the page.
const Http = new XMLHttpRequest();
Http.open("GET", '/Startup/Action/Test');
Http.send();
Http.onreadystatechange = (e) => {
console.log(Http.responseText)
}
Then in my Controller:
[HttpGet("Startup/Action/{Handle}")]
public IActionResult Action(string Handle)
{
this.ViewData["Result"] = Handle;
return this.Ok(Handle);
// return LocalRedirect("~/Startup/");
// return Redirect("http://localhost:50429/");
// return View("~/Views/Startup/Index.cshtml");
// return Redirect("/");
// return RedirectToAction("Index", "Startup");
}
I get response and see it in console written by the javascript XMLHttpRequest event. However I cannot redirect to just another page. I tried all options commented above as return value.
With AJAX you don't perform the redirect server-side, you perform it client-side after the result has been received. Something like this:
Http.onreadystatechange = (e) => {
if(xhr.readyState === 4 && xhr.status === 200) {
window.location.href = '#Url.Action("Index", "Startup")';
} else {
// non-success reaponse? do something else?
}
}
The above code would redirect following a successful AJAX response. Note that this JavaScript code would need to be on a view itself in order to use the Url.Action() helper. If the JavaScript code is in a separate file, one option could be to return as a string in the AJAX response the URL to which you are redirecting.
Note however that what you are doing is a bit different here:
this.ViewData["Result"] = Handle;
I could be wrong, but I suspect this won't carry over on a redirect. TempData might? But ultimately the question then becomes... What exactly are you trying to accomplish? You are sending a value to the server and then trying to redirect the user to another page to include that value. Well, the client already knows that value, and since it's AJAX the client needs to perform the redirect, so is this AJAX operation even necessary at all?
It's not entirely clear to me what the overall goal here is, and I suspect the overall setup could be simplified significantly. But it looks like AJAX is needed at all here. You can revert your server-side code to performing the redirect and, instead of using AJAX, just redirect the client to that server-side action entirely:
window.location.href = '/Startup/Action/Test';
or:
winfow.location.href = '#Url.Action("Action", "Startup", new { Handle = "Test" })';
And then redirect in your server-side action as you originally tried:
[HttpGet("Startup/Action/{Handle}")]
public IActionResult Action(string Handle)
{
this.ViewData["Result"] = Handle;
return RedirectToAction("Index", "Startup");
}
It's still possible, and again I'm not certain, that ViewData isn't correct here and you may want to use TempData on a redirect. Though, again, it's equally likely that whatever you're trying to accomplish can be done so more effectively in the first place. Such as skipping this Action action method entirely and just passing the Handle value directly to the Index action, whatever it does it with that value.

How to correctly handle the case where a stream is empty

How do I perform operations that have side effects in response to a stream being empty? For example, to handle the case where a client sends an empty body, and we read it with .bodyToMono(...).
See for example:
public Mono<ServerResponse> createEventDetails(ServerRequest request) {
return request.principal().flatMap(principal -> {
UserProfile userProfile = (UserProfile) ((UsernamePasswordAuthenticationToken) principal).getCredentials();
final String id = request.pathVariable("id");
final UUID eventId = UUID.fromString(id);
return request.bodyToMono(CreateEventDetailsRequest.class)
.map(body -> new CreateEventDetailsCommand(eventId, userProfile, body.getObjectId(), body.getDocumentUrls()))
.flatMap(command -> {
createEventDetailsCommandHandler.handle(command);
return ServerResponse
.created(URI.create(eventId.toString()))
.body(BodyInserters.fromObject(new CreateEventDetailsResponse(eventId)));
})
.switchIfEmpty(ServerResponse.badRequest().syncBody(new Error("missing request body for event id: " + id)))
.map(response -> {
LOG.error("POST createEventDetails: missing request body for event id: " + id);
return response;
});
});
}
This returns the desired 400 response in the case that the client omits the request body, but the log message is always printed, even for successful requests.
My best guess as to why this occurs, is that .switchIfEmpty(...).map(...) is executed as one unit by the framework, and then the result ignored.
How do I handle this case?
There are a few ways to do this. In the case above you can just add the logging to the switch
e.g.
.switchIfEmpty(ServerResponse.badRequest().syncBody(new Error("missing request body for event id: " + id))
.doOnNext(errorResponse -> LOG.error("POST createEventDetails: missing request body for event id: " + id)
)
another approach could be sending an Error signal back and in your handler catch and log it.
You can have a look here https://www.baeldung.com/spring-webflux-errors
Some light reading can be found here https://docs.spring.io/spring-framework/docs/5.0.0.BUILD-SNAPSHOT/javadoc-api/org/springframework/web/server/WebHandler.html
Use HttpWebHandlerAdapter to adapt a WebHandler to an HttpHandler. The
WebHttpHandlerBuilder provides a convenient way to do that while also
optionally configuring one or more filters and/or exception handlers.
You can view a sample handler in ResponseStatusExceptionHandler

Spring WebFlux Web Client - Iterating paged REST API

I need to get the items from all pages of a pageable REST API. I also need to start processing items, as soon as they are available, not needing to wait for all the pages to be loaded. In order to do so, I'm using Spring WebFlux and its WebClient, and want to return Flux<Item>.
Also, the REST API I'm using is rate limited, and each response to it contains headers with details on the current limits:
Size of the current window
Remaining time in the current window
Request quota in window
Requests left in current window
The response to a single page request looks like:
{
"data": [],
"meta": {
"pagination": {
"total": 10,
"current": 1
}
}
}
The data array contains the actual items, while the meta object contains pagination info.
My current solution first does a "dummy" request, just to get the total number of pages, and the rate limits.
Mono<T> paginated = client.get()
.uri(uri)
.exchange()
.flatMap(response -> {
HttpHeaders headers = response.headers().asHttpHeaders();
Limits limits = new Limits();
limits.setWindowSize(headers.getFirst("X-Window-Size"));
limits.setWindowRemaining(headers.getFirst("X-Window-Remaining"));
limits.setRequestsQuota(headers.getFirst("X-Requests-Quota");
limits.setRequestsLeft(headers.getFirst("X-Requests-Remaining");
return response.bodyToMono(Paginated.class)
.map(paginated -> {
paginated.setLimits(limits);
return paginated;
});
});
Afterwards, I emit a Flux containing page numbers, and for each page, I do a REST API request, each request being delayed enough so it doesn't get past the limit, and return a Flux of extracted items:
return paginated.flatMapMany(paginated -> {
return Flux.range(1, paginated.getMeta().getPagination().getTotal())
.delayElements(Duration.ofMillis(paginated.getLimits().getWindowRemaining() / paginated.getLimits().getRequestsQuota()))
.flatMap(page -> {
return client.get()
.uri(pageUri)
.retrieve()
.bodyToMono(Item.class)
.flatMapMany(p -> Flux.fromIterable(p.getData()));
});
});
This does work, but I'm not happy with it because:
It does initial "dummy" request to get the number of pages, and then
repeats the same request to get the actual data.
It gets rate limits only with the initial request, and assumes the
limits won't change (eg, that it's the only one using the API) -
which may not be true, in which case it will get an error that it
exceeded the limit.
So my question is how to refactor it so it doesn't need the initial request (but rather get limits, page numbers and data from the first request, and continue through all pages, while updating (and respecting) the limits.
I think this code will do what you want. The idea is to make a flux that make a call to your resource server, but in the process to handle the response, to add a new event on that flux to be able to make the call to next page.
The code is composed of:
A simple wrapper to contains the next page to call and the delay to wait before executing the call
private class WaitAndNext{
private String next;
private long delay;
}
A FluxProcessor that will make HTTP call and process the response:
FluxProcessor<WaitAndNext, WaitAndNext> processor= DirectProcessor.<WaitAndNext>create();
FluxSink<WaitAndNext> sink=processor.sink();
processor
.flatMap(x-> Mono.just(x).delayElement(Duration.ofMillis(x.delay)))
.map(x-> WebClient.builder()
.baseUrl(x.next)
.defaultHeader("Accept","application/json")
.build())
.flatMap(x->x.get()
.exchange()
.flatMapMany(z->manageResponse(sink, z))
)
.subscribe(........);
I split the code with a method that only manage response: It simply unwrap your data AND add a new event to the sink (the event beeing the next page to call after the given delay)
private Flux<Data> manageResponse(FluxSink<WaitAndNext> sink, ClientResponse resp) {
if (resp.statusCode()!= HttpStatus.OK){
sink.error(new IllegalStateException("Status code invalid"));
}
WaitAndNext wn=new WaitAndNext();
HttpHeaders headers=resp.headers().asHttpHeaders();
wn.delay= Integer.parseInt(headers.getFirst("X-Window-Remaining"))/ Integer.parseInt(headers.getFirst("X-Requests-Quota"));
return resp.bodyToMono(Item.class)
.flatMapMany(p -> {
if (p.paginated.current==p.paginated.total){
sink.complete();
}else{
wn.next="https://....?page="+(p.paginated.current+1);
sink.next(wn);
}
return Flux.fromIterable(p.getData());
});
}
Now we just need to initialize the system by calling for the retrieval of the first page with no delay:
WaitAndNext wn=new WaitAndNext();
wn.next="https://....?page=1";
wn.delay=0;
sink.next(wn);

Dojo datagrid jsonrest response headers

I'd like to use custom headers to provide some more information about the response data. Is it possible to get the headers in a response from a dojo datagrid hooked up to a jsonRest object via an object store (dojo 1.7)? I see this is possible when you are making the XHR request, but in this case it is being made by the grid.
The API provides an event for a response error which returns the response object:
on(this.grid, 'FetchError', function (response, req) {
var header = response.xhr.getAllResponseHeaders();
});
using this I am successfully able to access my custom response headers. However, there doesn't appear to be a way to get the response object when the request is successful. I have been using the undocumented private event _onFetchComplete with aspect after, however, this does not allow access to the response object, just the response values
aspect.after(this.grid, '_onFetchComplete', function (response, request)
{
///unable to get headers, response is the returned values
}, true);
Edit:
I managed to get something working, but I suspect it is very over engineered and someone with a better understanding could come up with a simpler solution. I ended up adding aspect around to allow me to get hold of the deferred object in the rest store which is returned to the object store. Here I added a new function to the deffered to return the headers. I then hooked in to the onFetch of the object store using dojo hitch (because I needed the results in the current scope). It seems messy to me
aspect.around(restStore, "query", function (original) {
return function (method, args) {
var def = original.call(this, method, args);
def.headers = deferred1.then(function () {
var hd = def.ioArgs.xhr.getResponseHeader("myHeader");
return hd;
});
return def;
};
});
aspect.after(objectStore, 'onFetch', lang.hitch(this, function (response) {
response.headers.then(lang.hitch(this, function (evt) {
var headerResult = evt;
}));
}), true);
Is there a better way?
I solved this today after reading this post, thought I'd feed back.
dojo/store/JsonRest solves it also but my code ended up slightly different.
var MyStore = declare(JsonRest, {
query: function () {
var results = this.inherited(arguments);
console.log('Results: ', results);
results.response.then(function (res) {
var myheader = res.xhr.getResponseHeader('My-Header');
doSomethingWith(myheader);
});
return results;
}
});
So you override the normal query() function, let it execute and return its promise, and attach your own listener to its 'response' member resolving, in which you can access the xhr object that has the headers. This ought to let you interpret the JsonRest result while fitting nicely into the chain of the query() all invokers.
One word of warning, this code is modified for posting here, and actually inherited from another intermediary class that also overrode query(), but the basics here are pretty sound.
If what you want is to get info from the server, also a custom key-value in the cookie can be a solution, that was my case, first I was looking for a custom response header but I couldn't make it work so I did the cookie way getting the info after the grid data is fetched:
dojo.connect(grid, "_onFetchComplete", function (){
doSomethingWith(dojo.cookie("My-Key"));
});
This is useful for example to present a SUM(field) for all rows in a paginated datagrid, and not only those included in the current page. In the server you can fetch the COUNT and the SUM, the COUNT will be sent in the Content-Range header and the SUM can be sent in the cookie.