Kathy
Kathy

Reputation: 116

Spring-boot asynchronous Request - Response body is empty

We have a Rest Controller that returns a WebAsyncTask. The code is as follows:

@RequestMapping(value="/test", produces="text/plain", method=RequestMethod.POST)
@ResponseBody
public WebAsyncTask<ResponseEntity<String>> test(@RequestParam(value="foo", required=false) String data) throws IOException {
  Callable<ResponseEntity<String>> callable=() -> {
    HttpHeadersresponseHeaders = newHttpHeaders();
    responseHeaders.setContentType(MediaType.TEXT_PLAIN);
    return new ResponseEntity("testping",responseHeaders,HttpStatus.OK);
  };
 
  WebAsyncTask<ResponseEntity<String>> webAsyncTask = new WebAsyncTask<>(getTimeOut(), getAsyncTaskExecutor(), callable);
  return webAsyncTask;
}

AsyncTaskExecutor is defined as follows:

<bean id="asyncTaskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
  <propertyname="corePoolSize" value="50"/>
  <propertyname="maxPoolSize" value="50"/>
  <propertyname="queueCapacity" value="5000"/>
</bean>

This is my Boot Application:

@SpringBootApplication(exclude={JacksonAutoConfiguration.class,
CodecsAutoConfiguration.class,HttpMessageConvertersAutoConfiguration.class,
HibernateJpaAutoConfiguration.class,WebMvcAutoConfiguration.class})
@ImportResource(locations={"classpath:META-INF/spring/test-boot-applicationContext.xml"})
@EnableWebSecurity
@EnableAsync
public class TestBootApplication extends SpringBootServletInitializer {
  public static void main(String[] args) {
    SpringApplication.run(TestBootApplication.class, args);
  }
 
  @Configuration
  public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurityhttpSecurity) throws Exception {
    httpSecurity
      .authorizeRequests().antMatchers("/**").permitAll()
      .and().csrf().disable();
    }
  }
 
  @Bean
  public ServletRegistrationBean dispatcherController() {
    DispatcherServlet dispatcherServlet = new DispatcherServlet();
    dispatcherServlet.setNamespace("tempspace");
    XmlWebApplicationContext applicationContext = new XmlWebApplicationContext();
    applicationContext.setConfigLocations("classpath:META-INF/spring/test-dispatcher-servlet.xml");
    dispatcherServlet.setApplicationContext(applicationContext);
    ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(dispatcherServlet,"/temp/*");
    servletRegistrationBean.setName("testdispatcher");
    servletRegistrationBean.setLoadOnStartup(20);
    return servletRegistrationBean;
  }
 
  @Bean
  public ObjectMapper defaultObjectMapper() {
    return new ObjectMapper();
  }
}

I am using Postman to send the POST request and I get a 200 OK response, which is just an acknowledgement and not the actual response:

KEY.                                                  VALUE
X-Frame-Options                                       DENY
Content-Security-Policy                               frame-ancestors 'none'
X-Content-Type-Options                                nosniff
X-XSS-Protection                                      1; mode=block
Content-Length                                        0
Date                                                  Thu, 29 Dec 2022 09:33:38 GMT
Keep-Alive                                            timeout=60
Connection                                            keep-alive

So, after a little investigation, I understood that I had to add DispatcherType ASYNC to my filter. This is my Filter:

@Component
public class AsyncFilter implements Filter {
  private static final Logger LOG = LoggerFactory.getLogger(AsyncFilter.class);
 
  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request);
    ContentCachingResponseWrapper responseWrapper = newContentCachingResponseWrapper((HttpServletResponse) response);
    try {
      filterChain.doFilter(requestWrapper,responseWrapper);
    } finally {
      LOG.trace("Before - ResponseWrapper: {}", 
      getResponsePayload(responseWrapper));
      LOG.trace("Before - Response: {}", 
      getResponsePayload((HttpServletResponse) response));
 
      byte[] responseArray = responseWrapper.getContentAsByteArray();
      String responseString = new String(responseArray, 
      responseWrapper.getCharacterEncoding());
      LOG.trace("####### responseString = {}", responseString);
 
      responseWrapper.copyBodyToResponse();
 
      LOG.trace("After - ResponseWrapper: {}", 
      getResponsePayload(responseWrapper));
      LOG.trace("After - Response: {}", 
      getResponsePayload((HttpServletResponse) response));
    }
  }

This is how I am registering the Filter in the TestBootApplication:

@Bean
public FilterRegistrationBean<AsyncFilter> asyncFilter() {
 FilterRegistrationBean<AsyncFilter> registrationBean = new FilterRegistrationBean<>();
 registrationBean.setFilter(new AsyncFilter());
registrationBean.setDispatcherTypes(EnumSet.of(DispatcherType.REQUEST, DispatcherType.ASYNC));
 registrationBean.addUrlPatterns("/*");
 return registrationBean;
}

Now I get the actual response but I get an empty body. Also, the content-length is 0.

KEY.                                                  VALUE
X-Frame-Options                                       DENY
Content-Security-Policy                               frame-ancestors 'none'
X-Content-Type-Options                                nosniff
X-XSS-Protection                                      1; mode=block
Content-Type                                          text/plain
Content-Length                                        0
Date                                                  Thu, 29 Dec 2022 09:33:38 GMT
Keep-Alive                                            timeout=60
Connection                                            keep-alive

Here is an excerpt from the logs:

16:57:11.099 [http-nio-8081-exec-5] TRACE c.b.a.filters.AsyncFilter - Before - Response Wrapper: testping
16:57:11.100 [http-nio-8081-exec-5] TRACE c.b.a.filters.AsyncFilter - Before - Response: [unknown]
16:57:11.100 [http-nio-8081-exec-5] TRACE c.b.a.filters.AsyncFilter - ####### Response = testping
16:57:11.100 [http-nio-8081-exec-5] TRACE c.b.a.filters.AsyncFilter - After - Response Wrapper: [unknown]
16:57:11.100 [http-nio-8081-exec-5] TRACE c.b.a.filters.AsyncFilter - After - Response: [unknown]

I think the response is being dispatched before the controller finishes processing. I have invested a lot of time on this and haven't been able to find out the cause. What am I missing here? I am using spring-boot version 2.6.12

Upvotes: 1

Views: 704

Answers (1)

times29
times29

Reputation: 3373

You could try omitting the WebAsyncTask and diretly return the Callable from the controller method like this:

    public Callable<ResponseEntity<String>> test(@RequestParam(value = "foo", required = false) String data) {
        return () -> {
            HttpHeaders responseHeaders = new HttpHeaders();
            responseHeaders.setContentType(MediaType.TEXT_PLAIN);
            return new ResponseEntity("testping", responseHeaders, HttpStatus.OK);
        };
    }

To configure your async task executor, you can create a global config bean like this:

@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {

    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(1);
        executor.setMaxPoolSize(10);
        executor.setThreadNamePrefix("mvc-task-executor-");
        executor.initialize();
        configurer.setTaskExecutor(executor);
    }

}

Side note: you can boost your chances of good answers if you format your code properly for SO.

Upvotes: 1

Related Questions