Skip to content

Commit 2aca714

Browse files
committed
Add locking in StandardServletAsyncWebRequest
The lock protects against race between onError/onComplete notifications and operations on the ServletOutputStream. See gh-32342
1 parent 3b7c435 commit 2aca714

File tree

1 file changed

+58
-25
lines changed

1 file changed

+58
-25
lines changed

spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java

Lines changed: 58 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import java.io.IOException;
2020
import java.util.ArrayList;
2121
import java.util.List;
22-
import java.util.concurrent.atomic.AtomicReference;
22+
import java.util.concurrent.locks.ReentrantLock;
2323
import java.util.function.Consumer;
2424

2525
import javax.servlet.AsyncContext;
@@ -34,6 +34,7 @@
3434
import org.springframework.lang.Nullable;
3535
import org.springframework.util.Assert;
3636
import org.springframework.web.context.request.ServletWebRequest;
37+
import org.springframework.web.util.DisconnectedClientHelper;
3738

3839
/**
3940
* A Servlet implementation of {@link AsyncWebRequest}.
@@ -60,9 +61,9 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
6061
@Nullable
6162
private AsyncContext asyncContext;
6263

63-
private final AtomicReference<State> state;
64+
private State state;
6465

65-
private volatile boolean hasError;
66+
private final ReentrantLock stateLock = new ReentrantLock();
6667

6768

6869
/**
@@ -87,13 +88,7 @@ public StandardServletAsyncWebRequest(HttpServletRequest request, HttpServletRes
8788

8889
super(request, new LifecycleHttpServletResponse(response));
8990

90-
if (previousRequest != null) {
91-
this.state = previousRequest.state;
92-
this.hasError = previousRequest.hasError;
93-
}
94-
else {
95-
this.state = new AtomicReference<>(State.ACTIVE);
96-
}
91+
this.state = (previousRequest != null ? previousRequest.state : State.ACTIVE);
9792

9893
//noinspection DataFlowIssue
9994
((LifecycleHttpServletResponse) getResponse()).setAsyncWebRequest(this);
@@ -137,7 +132,7 @@ public boolean isAsyncStarted() {
137132
*/
138133
@Override
139134
public boolean isAsyncComplete() {
140-
return (this.state.get() == State.COMPLETED);
135+
return (this.state == State.COMPLETED);
141136
}
142137

143138
@Override
@@ -184,20 +179,41 @@ public void onTimeout(AsyncEvent event) throws IOException {
184179

185180
@Override
186181
public void onError(AsyncEvent event) throws IOException {
187-
transitionToErrorState();
188-
this.exceptionHandlers.forEach(consumer -> consumer.accept(event.getThrowable()));
182+
this.stateLock.lock();
183+
try {
184+
transitionToErrorState();
185+
Throwable ex = event.getThrowable();
186+
this.exceptionHandlers.forEach(consumer -> consumer.accept(ex));
187+
188+
// We skip ASYNC dispatches for "disconnected client" errors,
189+
// but can only complete from a Servlet container thread
190+
191+
if (DisconnectedClientHelper.isClientDisconnectedException(ex) && this.state != State.COMPLETED) {
192+
this.asyncContext.complete();
193+
}
194+
}
195+
finally {
196+
this.stateLock.unlock();
197+
}
189198
}
190199

191200
private void transitionToErrorState() {
192-
this.hasError = true;
193-
this.state.compareAndSet(State.ACTIVE, State.ERROR);
201+
if (this.state == State.ACTIVE) {
202+
this.state = State.ERROR;
203+
}
194204
}
195205

196206
@Override
197207
public void onComplete(AsyncEvent event) throws IOException {
198-
this.completionHandlers.forEach(Runnable::run);
199-
this.asyncContext = null;
200-
this.state.set(State.COMPLETED);
208+
this.stateLock.lock();
209+
try {
210+
this.completionHandlers.forEach(Runnable::run);
211+
this.asyncContext = null;
212+
this.state = State.COMPLETED;
213+
}
214+
finally {
215+
this.stateLock.unlock();
216+
}
201217
}
202218

203219

@@ -256,59 +272,76 @@ public boolean isReady() {
256272

257273
@Override
258274
public void setWriteListener(WriteListener writeListener) {
275+
throw new UnsupportedOperationException();
259276
}
260277

261278
@Override
262279
public void write(int b) throws IOException {
263-
checkState();
280+
obtainLockAndCheckState();
264281
try {
265282
this.delegate.getOutputStream().write(b);
266283
}
267284
catch (IOException ex) {
268285
handleIOException(ex, "ServletOutputStream failed to write");
269286
}
287+
finally {
288+
releaseLock();
289+
}
270290
}
271291

272292
public void write(byte[] buf, int offset, int len) throws IOException {
273-
checkState();
293+
obtainLockAndCheckState();
274294
try {
275295
this.delegate.getOutputStream().write(buf, offset, len);
276296
}
277297
catch (IOException ex) {
278298
handleIOException(ex, "ServletOutputStream failed to write");
279299
}
300+
finally {
301+
releaseLock();
302+
}
280303
}
281304

282305
@Override
283306
public void flush() throws IOException {
284-
checkState();
307+
obtainLockAndCheckState();
285308
try {
286309
this.delegate.getOutputStream().flush();
287310
}
288311
catch (IOException ex) {
289312
handleIOException(ex, "ServletOutputStream failed to flush");
290313
}
314+
finally {
315+
releaseLock();
316+
}
291317
}
292318

293319
@Override
294320
public void close() throws IOException {
295-
checkState();
321+
obtainLockAndCheckState();
296322
try {
297323
this.delegate.getOutputStream().close();
298324
}
299325
catch (IOException ex) {
300326
handleIOException(ex, "ServletOutputStream failed to close");
301327
}
328+
finally {
329+
releaseLock();
330+
}
302331
}
303332

304-
private void checkState() throws AsyncRequestNotUsableException {
305-
if (this.asyncWebRequest.state.get() != State.ACTIVE) {
333+
private void obtainLockAndCheckState() throws AsyncRequestNotUsableException {
334+
if (!this.asyncWebRequest.stateLock.tryLock() || this.asyncWebRequest.state != State.ACTIVE) {
306335
throw new AsyncRequestNotUsableException("Response not usable after " +
307-
(this.asyncWebRequest.state.get() == State.COMPLETED ?
336+
(this.asyncWebRequest.state == State.COMPLETED ?
308337
"async request completion" : "onError notification") + ".");
309338
}
310339
}
311340

341+
private void releaseLock() {
342+
this.asyncWebRequest.stateLock.unlock();
343+
}
344+
312345
private void handleIOException(IOException ex, String msg) throws AsyncRequestNotUsableException {
313346
this.asyncWebRequest.transitionToErrorState();
314347
throw new AsyncRequestNotUsableException(msg, ex);

0 commit comments

Comments
 (0)