Guard against ConcurrentModificationException
when the response processes commitActions
#27587
Description
Affects: <Spring Framework version>v5.3.8
- Version :
spring-web
v5.3.8 - Server type : reactive
- Java class :
org.springframework.http.server.reactive.AbstractServerHttpResponse
- Method :
protected Mono<Void> doCommit(@Nullable Supplier<? extends Mono<Void>> writeAction) {
Hello,
Sometimes there is a race condition between beforeCommit
statements for the same request.
This error only arises during traffic peaks
Both HttpHeaderWriterWebFilter
and DefaultWebSessionManager
add commitActions
to the response
https://github.com/spring-projects/spring-security-reactive/blob/37749a64f782c2b2f81afb3db1b30cea3e956839/spring-security-reactive/src/main/java/org/springframework/security/web/server/header/HttpHeaderWriterWebFilter.java#L42
But sometimes (fortunately it is not happening a lot) DefaultWebSessionManager
adds its commitAction after the method doCommit
has started.
As a result,
doCommit
does Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get))
, which immediately stores an iterator over commitActions.
Then DefaultWebSessionManager
adds its commitAction,
which means that this.commitActions has size 2, but the iterator was built for a list of 1.
When the FluxIterable subscribes, it does iterator.next, which invokes ArrayList$Itr.checkForComodification
This is where I get the ConcurrentModificationException
.
To illustrate the problem, I am providing you this test class in kotlin.
This test systematically fails, but there is no race condition, it is simply a beforeCommit that takes place during another one, making the code build an iterator on a list of 1 when the list size increases to 2.
package mypackage
import org.junit.jupiter.api.Test
import org.springframework.mock.http.server.reactive.MockServerHttpResponse
import reactor.core.publisher.Mono
class AbstractServerHttpResponseTest {
@Test
fun test1() {
MockServerHttpResponse()
.apply {
beforeCommit {
beforeCommit {
Mono.empty()
}
Mono.empty()
}
}
.setComplete().block();
}
}
If it is an app-level error, can we think of a better error handling to avoid such problems ?
Otherwise, if it is a framework-level error, I would be happy to contribute, just let me know.