Skip to content

Commit 52c4ffa

Browse files
committed
Merge branch '6.1.x'
2 parents 940eef0 + debba65 commit 52c4ffa

File tree

5 files changed

+145
-5
lines changed

5 files changed

+145
-5
lines changed

spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
import java.util.stream.Collectors;
4545

4646
import jakarta.servlet.AsyncContext;
47+
import jakarta.servlet.AsyncEvent;
48+
import jakarta.servlet.AsyncListener;
4749
import jakarta.servlet.DispatcherType;
4850
import jakarta.servlet.RequestDispatcher;
4951
import jakarta.servlet.ServletConnection;
@@ -920,7 +922,19 @@ public AsyncContext startAsync() {
920922
public AsyncContext startAsync(ServletRequest request, @Nullable ServletResponse response) {
921923
Assert.state(this.asyncSupported, "Async not supported");
922924
this.asyncStarted = true;
923-
this.asyncContext = new MockAsyncContext(request, response);
925+
MockAsyncContext newAsyncContext = new MockAsyncContext(request, response);
926+
if (this.asyncContext != null) {
927+
try {
928+
AsyncEvent startEvent = new AsyncEvent(newAsyncContext);
929+
for (AsyncListener asyncListener : this.asyncContext.getListeners()) {
930+
asyncListener.onStartAsync(startEvent);
931+
}
932+
}
933+
catch (IOException ex) {
934+
// ignore failures
935+
}
936+
}
937+
this.asyncContext = newAsyncContext;
924938
return this.asyncContext;
925939
}
926940

spring-test/src/test/java/org/springframework/mock/web/MockHttpServletRequestTests.java

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
import java.util.Locale;
3131
import java.util.Map;
3232

33+
import jakarta.servlet.AsyncContext;
34+
import jakarta.servlet.AsyncEvent;
35+
import jakarta.servlet.AsyncListener;
3336
import jakarta.servlet.http.Cookie;
3437
import org.junit.jupiter.api.Test;
3538

@@ -663,6 +666,44 @@ void httpHeaderFormattedDateError() {
663666
request.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE));
664667
}
665668

669+
@Test
670+
void shouldRejectAsyncStartsIfUnsupported() {
671+
assertThat(request.isAsyncStarted()).isFalse();
672+
assertThatIllegalStateException().isThrownBy(request::startAsync);
673+
}
674+
675+
@Test
676+
void startAsyncShouldUpdateRequestState() {
677+
assertThat(request.isAsyncStarted()).isFalse();
678+
request.setAsyncSupported(true);
679+
AsyncContext asyncContext = request.startAsync();
680+
assertThat(request.isAsyncStarted()).isTrue();
681+
}
682+
683+
@Test
684+
void shouldNotifyAsyncListeners() {
685+
request.setAsyncSupported(true);
686+
AsyncContext asyncContext = request.startAsync();
687+
TestAsyncListener testAsyncListener = new TestAsyncListener();
688+
asyncContext.addListener(testAsyncListener);
689+
asyncContext.complete();
690+
assertThat(testAsyncListener.events).hasSize(1);
691+
assertThat(testAsyncListener.events.get(0)).extracting("name").isEqualTo("onComplete");
692+
}
693+
694+
@Test
695+
void shouldNotifyAsyncListenersWhenNewAsyncStarted() {
696+
request.setAsyncSupported(true);
697+
AsyncContext asyncContext = request.startAsync();
698+
TestAsyncListener testAsyncListener = new TestAsyncListener();
699+
asyncContext.addListener(testAsyncListener);
700+
AsyncContext newAsyncContext = request.startAsync();
701+
assertThat(testAsyncListener.events).hasSize(1);
702+
ListenerEvent listenerEvent = testAsyncListener.events.get(0);
703+
assertThat(listenerEvent).extracting("name").isEqualTo("onStartAsync");
704+
assertThat(listenerEvent.event.getAsyncContext()).isEqualTo(newAsyncContext);
705+
}
706+
666707
private void assertEqualEnumerations(Enumeration<?> enum1, Enumeration<?> enum2) {
667708
int count = 0;
668709
while (enum1.hasMoreElements()) {
@@ -672,4 +713,31 @@ private void assertEqualEnumerations(Enumeration<?> enum1, Enumeration<?> enum2)
672713
}
673714
}
674715

716+
static class TestAsyncListener implements AsyncListener {
717+
718+
List<ListenerEvent> events = new ArrayList<>();
719+
720+
@Override
721+
public void onComplete(AsyncEvent asyncEvent) throws IOException {
722+
this.events.add(new ListenerEvent("onComplete", asyncEvent));
723+
}
724+
725+
@Override
726+
public void onTimeout(AsyncEvent asyncEvent) throws IOException {
727+
this.events.add(new ListenerEvent("onTimeout", asyncEvent));
728+
}
729+
730+
@Override
731+
public void onError(AsyncEvent asyncEvent) throws IOException {
732+
this.events.add(new ListenerEvent("onError", asyncEvent));
733+
}
734+
735+
@Override
736+
public void onStartAsync(AsyncEvent asyncEvent) throws IOException {
737+
this.events.add(new ListenerEvent("onStartAsync", asyncEvent));
738+
}
739+
}
740+
741+
record ListenerEvent(String name, AsyncEvent event) {}
742+
675743
}

spring-web/src/main/java/org/springframework/web/filter/ServerHttpObservationFilter.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,13 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
119119
throw ex;
120120
}
121121
finally {
122-
// If async is started, register a listener for completion notification.
123-
if (request.isAsyncStarted()) {
122+
// If async is started during the first dispatch, register a listener for completion notification.
123+
if (request.isAsyncStarted() && request.getDispatcherType() == DispatcherType.REQUEST) {
124124
request.getAsyncContext().addListener(new ObservationAsyncListener(observation));
125125
}
126126
// scope is opened for ASYNC dispatches, but the observation will be closed
127127
// by the async listener.
128-
else if (request.getDispatcherType() != DispatcherType.ASYNC){
128+
else if (!isAsyncDispatch(request)) {
129129
Throwable error = fetchException(request);
130130
if (error != null) {
131131
observation.error(error);
@@ -180,6 +180,7 @@ public ObservationAsyncListener(Observation currentObservation) {
180180

181181
@Override
182182
public void onStartAsync(AsyncEvent event) {
183+
event.getAsyncContext().addListener(this);
183184
}
184185

185186
@Override

spring-web/src/test/java/org/springframework/web/filter/ServerHttpObservationFilterTests.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,16 @@
2222
import io.micrometer.observation.ObservationRegistry;
2323
import io.micrometer.observation.tck.TestObservationRegistry;
2424
import io.micrometer.observation.tck.TestObservationRegistryAssert;
25+
import jakarta.servlet.AsyncContext;
2526
import jakarta.servlet.AsyncEvent;
2627
import jakarta.servlet.AsyncListener;
2728
import jakarta.servlet.DispatcherType;
29+
import jakarta.servlet.Filter;
30+
import jakarta.servlet.FilterChain;
2831
import jakarta.servlet.RequestDispatcher;
2932
import jakarta.servlet.ServletException;
33+
import jakarta.servlet.ServletRequest;
34+
import jakarta.servlet.ServletResponse;
3035
import jakarta.servlet.http.HttpServlet;
3136
import jakarta.servlet.http.HttpServletRequest;
3237
import jakarta.servlet.http.HttpServletResponse;
@@ -149,6 +154,21 @@ void shouldCloseObservationAfterAsyncCompletion() throws Exception {
149154
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS").hasBeenStopped();
150155
}
151156

157+
@Test
158+
void shouldRegisterListenerForAsyncStarts() throws Exception {
159+
CustomAsyncFilter customAsyncFilter = new CustomAsyncFilter();
160+
this.mockFilterChain = new MockFilterChain(new NoOpServlet(), customAsyncFilter);
161+
this.request.setAsyncSupported(true);
162+
this.request.setDispatcherType(DispatcherType.REQUEST);
163+
this.filter.doFilter(this.request, this.response, this.mockFilterChain);
164+
customAsyncFilter.asyncContext.dispatch();
165+
this.request.setDispatcherType(DispatcherType.ASYNC);
166+
AsyncContext newAsyncContext = this.request.startAsync();
167+
newAsyncContext.complete();
168+
169+
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS").hasBeenStopped();
170+
}
171+
152172
@Test
153173
void shouldCloseObservationAfterAsyncError() throws Exception {
154174
this.request.setAsyncSupported(true);
@@ -210,6 +230,29 @@ protected void onScopeOpened(Observation.Scope scope, HttpServletRequest request
210230
Assert.notNull(response, "response must not be null");
211231
response.setHeader("X-Trace-Id", "badc0ff33");
212232
}
233+
234+
}
235+
236+
@SuppressWarnings("serial")
237+
static class NoOpServlet extends HttpServlet {
238+
239+
@Override
240+
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
241+
242+
}
243+
244+
}
245+
246+
static class CustomAsyncFilter implements Filter {
247+
248+
AsyncContext asyncContext;
249+
250+
@Override
251+
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
252+
this.asyncContext = servletRequest.startAsync();
253+
filterChain.doFilter(servletRequest, servletResponse);
254+
}
255+
213256
}
214257

215258
}

spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletRequest.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
import java.util.stream.Collectors;
4545

4646
import jakarta.servlet.AsyncContext;
47+
import jakarta.servlet.AsyncEvent;
48+
import jakarta.servlet.AsyncListener;
4749
import jakarta.servlet.DispatcherType;
4850
import jakarta.servlet.RequestDispatcher;
4951
import jakarta.servlet.ServletConnection;
@@ -920,7 +922,19 @@ public AsyncContext startAsync() {
920922
public AsyncContext startAsync(ServletRequest request, @Nullable ServletResponse response) {
921923
Assert.state(this.asyncSupported, "Async not supported");
922924
this.asyncStarted = true;
923-
this.asyncContext = new MockAsyncContext(request, response);
925+
MockAsyncContext newAsyncContext = new MockAsyncContext(request, response);
926+
if (this.asyncContext != null) {
927+
try {
928+
AsyncEvent startEvent = new AsyncEvent(newAsyncContext);
929+
for (AsyncListener asyncListener : this.asyncContext.getListeners()) {
930+
asyncListener.onStartAsync(startEvent);
931+
}
932+
}
933+
catch (IOException ex) {
934+
// ignore failures
935+
}
936+
}
937+
this.asyncContext = newAsyncContext;
924938
return this.asyncContext;
925939
}
926940

0 commit comments

Comments
 (0)