Please consider the following test (the entire test code is provided below): it is comprised of the WebSessionTest which sends a request to the WebSessionController, the latter responds with a 200 response, afterwards the web session is being saved (see the WebSessionTestWebSessionManager) which results in the response before commit chain failure owing to the InMemoryWebSessionStore’s size is 0 (is employed by the WebSessionTestWebSessionManager). WebExceptionHandlers are invoked (no matter how an exception handler is implemented it may be either an annotated method or a WebExceptionHandler implementation) notwithstanding the client receives a 500 response.
I would like to return a 503 response with an application specific representation. I came up with a workaround in the DefaultWebSessionManager's overloaded method:
exchange.getResponse()
.beforeCommit(() -> Mono.defer(session::save)
.onErrorResume(this::isMaxSessionsLimitReachedException, e -> {
exchange.getResponse().setStatusCode(HttpStatus.SERVICE_UNAVAILABLE);
exchange.getResponse().getHeaders().clear();
exchange.getResponse().getCookies().clear();
return Mono.empty();
}));
however it doesn't give me a chance to provide a body otherwise the client hangs.
Environment: * Java 11, * Spring 5.2.8.RELEASE, * Spring Boot 2.3.3.RELEASE.
The code of the test:
WebSessionTest:
package com.example.session;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = {
WebSessionController.class,
WebSessionTestWebSessionManager.class,
WebSessionTestWebExceptionHandler.class
})
@EnableAutoConfiguration
public class WebSessionTest
{
@LocalServerPort
private int serverPort;
@Test
public void testJustReponse() throws ExecutionException, InterruptedException
{
long timestamp = System.currentTimeMillis();
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:" + serverPort + "/just/" + timestamp))
.build();
CompletableFuture<HttpResponse<String>> future =
client.sendAsync(request, BodyHandlers.ofString()).completeOnTimeout(null, 5, TimeUnit.SECONDS);
HttpResponse<String> response = future.get();
assertNotNull(response, "No response in 5 seconds.");
assertEquals(503, response.statusCode());
}
}
WebSessionController:
package com.example.session;
import java.util.concurrent.Executors;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.WebSession;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
import static org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE;
@RestController
public class WebSessionController implements InitializingBean, DisposableBean
{
private Scheduler scheduler;
@Override
public void afterPropertiesSet()
{
scheduler = Schedulers.fromExecutorService(Executors.newCachedThreadPool());
}
@Override
public void destroy()
{
scheduler.dispose();
}
@GetMapping("/just/{timestamp}")
public Mono<ResponseEntity<String>> just(@PathVariable String timestamp, WebSession session)
{
return Mono.fromCallable(() -> {
session.getAttributes().putIfAbsent("test", timestamp);
return ResponseEntity.status(HttpStatus.OK)
.header(HttpHeaders.CACHE_CONTROL, "no-store")
.body(timestamp);
}).subscribeOn(scheduler);
}
/*
No matter how the exception handler is implemented it may be either an annotated method or a WebExceptionHandler
implementation – the client invariably receives a 500 response.
@ResponseStatus(value= SERVICE_UNAVAILABLE, reason="Too many sessions")
@ExceptionHandler
public void tooManySessions(Exception e)
{
}
@ExceptionHandler
public Mono<ResponseEntity<String>> tooManySessions(Exception e)
{
return Mono.fromCallable(() -> ResponseEntity.status(500).body(e.getMessage())).subscribeOn(scheduler);
}
@ExceptionHandler
public ResponseEntity<String> tooManySessions(Exception e)
{
return ResponseEntity.status(503).body("To many sessions: " + e.getMessage());
}
*/
}
WebSessionTestWebSessionManager:
package com.example.session;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
import org.springframework.web.server.session.DefaultWebSessionManager;
import org.springframework.web.server.session.InMemoryWebSessionStore;
import org.springframework.web.server.session.WebSessionManager;
@Component("webSessionManager")
public class WebSessionTestWebSessionManager extends DefaultWebSessionManager implements WebSessionManager,
InitializingBean
{
private final InMemoryWebSessionStore sessionStore = new InMemoryWebSessionStore();
@Override
public void afterPropertiesSet()
{
sessionStore.setMaxSessions(0);
super.setSessionStore(sessionStore);
}
}
WebSessionTestWebExceptionHandler:
package com.example.session;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebExceptionHandler;
import reactor.core.publisher.Mono;
@Component
public class WebSessionTestWebExceptionHandler implements WebExceptionHandler
{
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable throwable)
{
exchange.getResponse().setStatusCode(HttpStatus.SERVICE_UNAVAILABLE);
exchange.getResponse().getHeaders().clear();
exchange.getResponse().getCookies().clear();
return exchange.getResponse()
.writeWith(Flux.just(exchange.getResponse()
.bufferFactory()
.wrap("Service unavailable".getBytes(StandardCharsets.UTF_8))));
}
}
Comment From: rstoyanchev
My initial analysis in https://github.com/spring-projects/spring-framework/issues/24186#issuecomment-690187094 is that we need to refine the solution from #24186 in order to skip pre-commit actions, when they have previously failed, and at least allow applying the current error response state.