Repository: wiremock/wiremock (language: java)

Issue:
Add a serve event listener triggered just before the response is sent
### Proposal

To support cases where we wish to respond to a serve event after the response has been rendered but before it has been sent. A significant advantage of this vs. using `afterComplete` is that this can be called synchronously, whereas `afterComplete` is async due to Jetty's use of non-blocking I/O.

### References

_No response_


Discussion / hints:

Fix patch:
diff --git a/src/main/java/com/github/tomakehurst/wiremock/extension/ServeEventListener.java b/src/main/java/com/github/tomakehurst/wiremock/extension/ServeEventListener.java
index 96b54895c2..011e396db6 100644
--- a/src/main/java/com/github/tomakehurst/wiremock/extension/ServeEventListener.java
+++ b/src/main/java/com/github/tomakehurst/wiremock/extension/ServeEventListener.java
@@ -22,6 +22,7 @@ public interface ServeEventListener extends Extension {
   enum RequestPhase {
     BEFORE_MATCH,
     AFTER_MATCH,
+    BEFORE_RESPONSE_SENT,
     AFTER_COMPLETE
   }
 
@@ -33,6 +34,9 @@ default void onEvent(RequestPhase requestPhase, ServeEvent serveEvent, Parameter
       case AFTER_MATCH:
         afterMatch(serveEvent, parameters);
         break;
+      case BEFORE_RESPONSE_SENT:
+        beforeResponseSent(serveEvent, parameters);
+        break;
       case AFTER_COMPLETE:
         afterComplete(serveEvent, parameters);
         break;
@@ -43,6 +47,8 @@ default void beforeMatch(ServeEvent serveEvent, Parameters parameters) {}
 
   default void afterMatch(ServeEvent serveEvent, Parameters parameters) {}
 
+  default void beforeResponseSent(ServeEvent serveEvent, Parameters parameters) {}
+
   default void afterComplete(ServeEvent serveEvent, Parameters parameters) {}
 
   default boolean applyGlobally() {
diff --git a/src/main/java/com/github/tomakehurst/wiremock/http/StubRequestHandler.java b/src/main/java/com/github/tomakehurst/wiremock/http/StubRequestHandler.java
index 1c035b0a6b..4957ffff50 100644
--- a/src/main/java/com/github/tomakehurst/wiremock/http/StubRequestHandler.java
+++ b/src/main/java/com/github/tomakehurst/wiremock/http/StubRequestHandler.java
@@ -86,6 +86,8 @@ protected void beforeResponseSent(ServeEvent serveEvent, Response response) {
     }
 
     requestJournal.requestReceived(serveEvent);
+
+    triggerListeners(BEFORE_RESPONSE_SENT, serveEvent);
   }
 
   private void appendNonMatchSubEvent(ServeEvent serveEvent) {


Test patch:
diff --git a/src/test/java/com/github/tomakehurst/wiremock/ServeEventListenerExtensionTest.java b/src/test/java/com/github/tomakehurst/wiremock/ServeEventListenerExtensionTest.java
index 747748a74b..80f7ec63e7 100644
--- a/src/test/java/com/github/tomakehurst/wiremock/ServeEventListenerExtensionTest.java
+++ b/src/test/java/com/github/tomakehurst/wiremock/ServeEventListenerExtensionTest.java
@@ -27,6 +27,7 @@
 import static org.awaitility.Awaitility.await;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.*;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.github.tomakehurst.wiremock.admin.Router;
@@ -44,6 +45,7 @@
 import java.util.concurrent.Callable;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Test;
@@ -67,8 +69,8 @@ public void cleanup() {
   }
 
   @Test
-  void eventTriggeredBeforeMatching() throws Exception {
-    final CompletableFuture<Void> completed = new CompletableFuture<>();
+  void eventSynchronouslyTriggeredBeforeMatching() {
+    AtomicBoolean completed = new AtomicBoolean(false);
     initWithOptions(
         options()
             .dynamicPort()
@@ -82,7 +84,7 @@ public void beforeMatch(ServeEvent serveEvent, Parameters parameters) {
                     assertThat(serveEvent.getStubMapping(), nullValue());
                     assertThat(serveEvent.getResponse(), nullValue());
 
-                    completed.complete(null);
+                    completed.set(true);
                   }
 
                   @Override
@@ -95,12 +97,12 @@ public String getName() {
 
     client.get("/get-this");
 
-    completed.get(2, SECONDS);
+    assertTrue(completed.get());
   }
 
   @Test
-  void eventTriggeredAfterMatching() throws Exception {
-    final CompletableFuture<Void> completed = new CompletableFuture<>();
+  void eventSynchronouslyTriggeredAfterMatching() {
+    AtomicBoolean completed = new AtomicBoolean(false);
     initWithOptions(
         options()
             .dynamicPort()
@@ -114,7 +116,7 @@ public void afterMatch(ServeEvent serveEvent, Parameters parameters) {
                     assertThat(serveEvent.getStubMapping(), notNullValue());
                     assertThat(serveEvent.getResponse(), nullValue());
 
-                    completed.complete(null);
+                    completed.set(true);
                   }
 
                   @Override
@@ -127,11 +129,43 @@ public String getName() {
 
     client.get("/get-this");
 
-    completed.get(2, SECONDS);
+    assertTrue(completed.get());
+  }
+
+  @Test
+  void eventSynchronouslyTriggeredBeforeResponseSent() {
+    AtomicBoolean completed = new AtomicBoolean(false);
+    initWithOptions(
+        options()
+            .dynamicPort()
+            .extensions(
+                new ServeEventListener() {
+
+                  @Override
+                  public void beforeResponseSent(ServeEvent serveEvent, Parameters parameters) {
+                    assertThat(serveEvent.getRequest().getUrl(), is("/get-this"));
+                    assertThat(serveEvent.getResponseDefinition(), notNullValue());
+                    assertThat(serveEvent.getStubMapping(), notNullValue());
+                    assertThat(serveEvent.getResponse().getStatus(), is(200));
+
+                    completed.set(true);
+                  }
+
+                  @Override
+                  public String getName() {
+                    return "before-resposnse-sent";
+                  }
+                }));
+
+    wm.stubFor(any(anyUrl()).willReturn(ok()));
+
+    client.get("/get-this");
+
+    assertTrue(completed.get());
   }
 
   @Test
-  void eventTriggeredAfterCompletion() throws Exception {
+  void eventAsynchronouslyTriggeredAfterCompletion() throws Exception {
     final CompletableFuture<Void> completed = new CompletableFuture<>();
     initWithOptions(
         options()