Request Scope beans in servlet 3 async Controler - spring

I'm using a Servlet 3 controller in a Spring Boot application. This controller calls services that, in the end, make 3 HTTP requests.
#GetMapping("/nbasync/c/**")
public CompletableFuture<String> nonBlockingAsyncCall() throws Exception {
CompletableFuture<String> result = service.call());
CompletableFuture<Void> result2 = service2.call();
CompletableFuture<Void> result3 = service3.call();
return result
.thenCombine(result2, this::keepFirst)
.thenCombine(result3, this::keepFirst);
}
Each of this outgoing calls are made using a RestTemplate and are intercepted by a ClientHttpRequestInterceptor. In this ClientHttpRequestInterceptor, I need a (proxied) request scoped bean (cf: How to enable request scope in async task executor with a Runnable).
This works just fine if I wait for the the result :
CompletableFuture.allOf(result, result2, result3).join();
return result.get();
In the non blocking method, it crashes with the following exception :
java.util.concurrent.CompletionException:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.requestCookieHelper': Scope 'request' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; nested exception is
java.lang.IllegalStateException: Cannot ask for request attribute - request is not active anymore!
See complete log : https://gist.github.com/Skeebl/d0b19ebb9ab4d0d2a917203e4bd6fad5
It appears that each time a thread lets go of the process, AbstractRequestAttributes.requestCompleted() is called (twice). This methods sets this.requestActive = false;. While requestActive is false, you can't access the request scoped beans.
The interceptor and request scoped bean simplify the methods signatures. Is there a way to keep theses while working with async requests ?

Related

Error when calling session object inside Method decorated by Spring websocket annotations

Using the Grails spring-websocket plugin:
CurrentStatusController.groovy
#MessageMapping("/personExist")
#SendTo("/topic/personExist")
protected Boolean personExist(String personId) {
return (Person.get(personId)!=null)
}
Saving all id of persons in session list personIds, then handle the same method using a session object
CurrentStatusController.groovy
#MessageMapping("/personExist")
#SendTo("/topic/personExist")
protected Boolean personExist(String personId) {
return (session.personIds.contains(personId))
}
The first works, the last does not work with the following error message :
ERROR springwebsocket.GrailsSimpAnnotationMethodMessageHandler -
Unhandled exception Message: No thread-bound request found: Are you
referring to request attributes outside of an actual web request, or
processing a request outside of the originally receiving thread? If
you are actually operating within a web request and still receive this
message, your code is probably running outside of
DispatcherServlet/DispatcherPortlet: In this case, use
RequestContextListener or RequestContextFilter to expose the current
request.
Line | Method
->> 53 | getSession in org.grails.plugins.web.rest.api.ControllersRestApi
How to make Spring-WebSocket methods accepts session object?

Unable to get ServletContext in Spring Controller when using Websockets

Client Code:
$scope.socket = new SockJS("ws/ws");
$scope.stompClient = Stomp.over($scope.socket);
$scope.stompClient.connect("guest", "guest",connectCallback, errorCallback);
//in connectCallback
$scope.stompClient.subscribe('/topic/agent-sendstatus', showScreenPop);
Java Code:
#MessageMapping("/agent-sendstatus")
public void testmethod()
{
//How do i get ServletContext here to further implement solution?
template.convertAndSend("/topic/agent-sendstatus","bcd");
}
Please suggest.
I am getting session using SimpMessageHeaderAccessor in the controller but i need ServletContext too.
Is there some way in Spring?
When you upgrade to WebSocket there's no HTTP involved in the communication, the only place where you have HTTP is during the handshake. You could implement a HandshakeInterceptor to get the ServletContext and expose any parameter to the WebSocket session, so you can get them in your message handler method with a HeaderAccessor. Check out the HttpSessionHandshakeInterceptor for an example.

Grails: User logs out while ajax request is running

There is a Grails (v.2.3.2) web app with Spring Security Core plugin (v.2.0-RC2).
I stumbled upon an issue with users who log out while there is an ajax request running in the background.
The scenario is as follows:
User requests a web page
When the page is ready I fire an ajax request
User logs out while the ajax request is still being processed on the server side
The server side, naturally, heavily depends on the current user, and the app crushes on the third step because the current user suddenly disappears as the springSecurityService indicates that the user is not logged in.
This is the code I used to fetch the current user in the UserService.
public User getLoggedInUser() {
if (!springSecurityService.isLoggedIn()) {
return null
}
User user = User.get(springSecurityService.getPrincipal().id)
user
}
Which, returns the current user alright up until the moment the user logs out, causing the issue.
I came up with the idea to make the UserService stateful and store the current user in a separate field.
static scope = 'request' // create a new instance for every request
private Long currentUserId = null
public User getLoggedInUser() {
if (!currentUserId) {
if (!springSecurityService.isLoggedIn()) {
return null
}
// Store the ID of the current user in the instance variable.
currentUserId = springSecurityService.getPrincipal().id
}
// Fetch and return the user.
return User.get(currentUserId)
}
In addition, I created a new Spring bean which defines a proxy object for my UserService.
userServiceProxy(ScopedProxyFactoryBean) {
targetBeanName = 'userService'
proxyTargetClass = true
}
Now, this works very well for the most scenarios, but fails when there is no web request present. In particular, in BootStrap.groovy, where I use other services of my application.
This is the error message I get:
Error initializing the application: Error creating bean with name 'scopedTarget.userServiceProxy': Scope 'request' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; nested exception is java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
Any suggestions on how to fix this?
After some investigation and lots of swear words the solution was finally found.
This is the code I use in BootStrap.groovy to mimic an ongoing web request.
class BootStrap {
def init = { ServletContext servletContext ->
// Mock request and response.
HttpServletRequest request = new MockHttpServletRequest(servletContext)
HttpServletResponse response = new MockHttpServletResponse()
// Now store them in the current thread.
GrailsWebRequest grailsRequest = new GrailsWebRequest(request, response, servletContext)
WebUtils.storeGrailsWebRequest(grailsRequest)
/**
* Perform whatever you need to do that requires an active web request.
*/
}
}

Spring Asynchronous Processing Does not Return To View

I'm using the Spring MVC asyncronous processing but the controller does not return a view on web browser.
#RequestMapping(value = "/generateGM", method = RequestMethod.POST)
public Callable<ModelAndView> generateGMReport(#RequestParam("countryCode") int countryCode, ModelAndView mv) {
Callable<ModelAndView> c = new GenericCallable(countryCode, reportDao, mv);
return c;
}
#Override
public ModelAndView call() throws Exception {
List<CostReport> gmList = reportDao.generateGrossMarginReport(countryCode);
mv.setViewName("gmReport");
mv.addObject("gmList", gmList);
return mv;
}
I had tried to modify the code to return Callable but it still does not return to the specified view name.
I'm using JBoss 7.1 as.
There is warning during deployment :
WARN [org.jboss.as.ee] (MSC service thread 1-7)
JBAS011006: Not installing optional component
org.springframework.web.context.request.async.StandardServletAsyncWebRequest
due to exception: org.jboss.as.server.deployment.DeploymentUnitProcessingException:
JBAS011054:
Could not find default constructor for class
org.springframework.web.context.request.async.StandardServletAsyncWebRequest
Reason: Perhaps sitemesh cannot set the response object from Spring MVC framework (AsynContext).
What is the reason ?
Please help.
Thanks.
Since the Sitemesh filter does some post-processing at the end of a request, it needs to support the Servlet 3 async request feature in order for this to work. When the initial Servlet container thread exits and the response remains open. If the Sitemesh filter is unaware of this, it will attempt to complete processing to early.
I am not an expect on sitemesh. But it's a servlet also so they follow the "chain of command" pattern which means it's possible it fail to transfer the correct url you need. can you post you config for async spring and sitemesh config in web.xml
It may be helpful. Return as a String instead of ModelAndView.

webapi batching and delegating handlers

based on my last post I was able to get batching working... until a certain point. In addition to registering the route specific handler I also have 2 delegating handlers
Authenticate the user
logging
the batch handler goes through the delegating handlers authenticating the user and logging the request. when the messagehandlerinvoker starts to send the child/nested requests the following exception is thrown.
System.ArgumentException was unhandled by user code
HResult=-2147024809
Message=The 'DelegatingHandler' list is invalid because the property 'InnerHandler' of 'AuthenticationMessageHandler' is not null.
Parameter name: handlers
Source=System.Net.Http.Formatting
ParamName=handlers
StackTrace:
at System.Net.Http.HttpClientFactory.CreatePipeline(HttpMessageHandler innerHandler, IEnumerable`1 handlers)
at System.Web.Http.HttpServer.Initialize()
at System.Web.Http.HttpServer.<EnsureInitialized>b__3()
at System.Threading.LazyInitializer.EnsureInitializedCore[T](T& target, Boolean& initialized, Object& syncLock, Func`1 valueFactory)
at System.Threading.LazyInitializer.EnsureInitialized[T](T& target, Boolean& initialized, Object& syncLock, Func`1 valueFactory)
at System.Web.Http.HttpServer.EnsureInitialized()
at System.Web.Http.HttpServer.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
at System.Net.Http.HttpMessageInvoker.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
at RoutingRequest.Service.Startup.BatchMessageHandler.<>c__DisplayClassd.<PrcoessRequest>b__b(Task`1 m) in C:\CEI\Clients\Footlocker.com\FL - Vendor Routing Portal\source\RoutingRequest.Service\Startup\BatchMessageHandler.cs:line 45
at System.Threading.Tasks.ContinuationResultTaskFromResultTask`2.InnerInvoke()
at System.Threading.Tasks.Task.Execute()
InnerException:
is there a config option I am missing, or do I need to bypass the delegating handlers?
edit
here is my authentication handler.
public class AuthenticationMessageHandler
: DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
SetCurrentUser(request);
return base.SendAsync(request, cancellationToken);
}
private void SetCurrentUser(HttpRequestMessage request)
{
var values = new List<string>().AsEnumerable();
if (request.Headers.TryGetValues("routingrequest-username", out values) == false) return;
var username = values.First();
var user = Membership.GetUser(username, true);
if (user == null)
{
var message = string.Format("membership information for '{0}' could not be found.", username);
throw new HttpRequestException(message);
}
var roles = Roles.GetRolesForUser(username);
Thread.CurrentPrincipal = new GenericPrincipal(new GenericIdentity(user.UserName), roles);
}
}
based on Kiran's answer a subclassed httpserver fixes one issue and introduces another. My roles provider is getting a null reference exception. looking into that now.
That blog post correctly identifies the problem, but there is a simpler solution if you are configuring OWIN using a Startup or OwinStartup class:
Change the OWIN configuration call from
UseWebApi(this IAppBuilder builder, HttpConfiguration configuration);
to
UseWebApi(this IAppBuilder builder, HttpServer httpServer);
so that your batch handler and the OWIN pipeline are using the same HttpServer instance.
The root cause of this is that many of the batching articles/examples (eg http://bradwilson.typepad.com/blog/2012/06/batching-handler-for-web-api.html ) create a new HttpServer for batching in addition to the main HttpServer that is handling HTTP requests; and both HttpServers are using the same HttpConfiguration.
When each HttpServer is initialized the first time it receives requests, it creates a pipeline of handlers (in HttpClientFactory.CreatePipeline) by reversing all the configured delegating handlers (eg tracing handlers, or other proxy-type handlers), and terminating the pipeline with the Web API dispatcher.
If you don't have any delegating handlers configured, then this problem won't bite you - you can have 2 HttpServer objects that use the same HttpConfiguration.
But if you have any delegating handlers explicitly or implicitly configured (eg by enabling Web API Tracing), then Web API can't build the 2nd pipeline - the delegating handlers are already linked in the first pipeline - and this exception is thrown on the first request to the 2nd HttpServer.
This exception should absolutely be more clear about what is going on. Better yet, this problem shouldn't even be possible - configuration should be configuration, not individual handlers. The configuration could be a factory for delegating handlers. But I digress...
While the issue is kinda hard to figure out, there's a pretty easy fix:
If you're using OWIN, pass the same HttpServer as you use in the batch handler to the OWIN pipeline via UseWebApi(this IAppBuilder builder, HttpServer httpServer);
If you're using IIS + Web API (no OWIN Startup class), pass GlobalConfiguration.DefaultServer to your batch handler, to avoid creating a new HttpServer
Here's an example OWIN startup class that creates a single HttpServer and passes it to both the batch handler, and Web API. This example uses to OData batch handler:
[assembly: OwinStartup(typeof(My.Web.OwinStartup))]
namespace My.Web
{
/// <summary>
/// OWIN webapp configuration.
/// </summary>
public sealed class OwinStartup
{
/// <summary>
/// Configure all the OWIN modules that participate in each request.
/// </summary>
/// <param name="app">The OWIN appBuilder</param>
public void Configuration(IAppBuilder app)
{
HttpConfiguration webApiConfig = new HttpConfiguration();
webApiConfig.MapHttpAttributeRoutes();
HttpServer webApiServer = new HttpServer(webApiConfig);
// Configure batch handler
var batchHandler = new DefaultODataBatchHandler(webApiServer);
webApiConfig.Routes.MapODataServiceRoute("ODataRoute",
"odata",
BuildEdmModel(),
new DefaultODataPathHandler(),
ODataRoutingConventions.CreateDefault(),
batchHandler);
app.UseWebApi(webApiServer);
}
private EdmModel BuildEdmModel()
{
// ...
}
}
}
I've had this error without batching. I made an HttpClientFactory of my own and it takes in a HandlerFactory, also my own.
It calls the HandlerFactory.Create() method in the constructor and stores the resulting handlers that it made.
These are passed to the System.Net.Http.HttpClientFactory.Create(...) method whenever the factory needs to make a new HttpClient.
But it's then only good for a single call because the handlers themselves are mutated by the .NET code leaving them in a state that means they cannot be reused.
I altered my constructor so that it doesn't create the handlers up front, but each time. It now works.

Resources