Skip to content

Commit ff4c303

Browse files
committed
feat: Fire after-response hooks inside the request scope
DispatchHandler stashes the rendered Response as an exchange attribute so fireAfterHooks can pass the real response to global and per-request hooks. resolveResponse falls back to synthesising a Response from exchange headers/status on the error path. A try/finally around runInnerChain ensures hooks fire even when exceptionHandler.handle itself throws.
1 parent bc503f7 commit ff4c303

3 files changed

Lines changed: 47 additions & 5 deletions

File tree

src/main/java/com/retailsvc/http/Request.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,8 +336,8 @@ public void afterResponse(Runnable runnable) {
336336
afterHooks.add(runnable);
337337
}
338338

339-
/** Package-private accessor for the after-hook queue; used by RequestPreparationFilter. */
340-
List<Runnable> afterHooks() {
339+
/** Internal accessor for the after-hook queue; used by RequestPreparationFilter. */
340+
public List<Runnable> afterHooks() {
341341
return afterHooks;
342342
}
343343

src/main/java/com/retailsvc/http/internal/DispatchHandler.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
public final class DispatchHandler implements HttpHandler {
1616

1717
public static final ScopedValue<Request> CURRENT = ScopedValue.newInstance();
18+
public static final String RESPONSE_ATTR = "com.retailsvc.http.response";
1819

1920
private final Map<String, RequestHandler> handlers;
2021
private final List<RequestInterceptor> interceptors;
@@ -43,6 +44,7 @@ public void handle(HttpExchange exchange) throws IOException {
4344
for (ResponseDecorator decorator : decorators) {
4445
response = decorator.decorate(request, response);
4546
}
47+
exchange.setAttribute(RESPONSE_ATTR, response);
4648
renderer.render(exchange, response);
4749
}
4850

src/main/java/com/retailsvc/http/internal/RequestPreparationFilter.java

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
import com.retailsvc.http.validate.ValidationError;
1818
import com.retailsvc.http.validate.Validator;
1919
import com.sun.net.httpserver.Filter;
20+
import com.sun.net.httpserver.Headers;
2021
import com.sun.net.httpserver.HttpExchange;
2122
import java.io.IOException;
2223
import java.util.HashMap;
24+
import java.util.LinkedHashMap;
2325
import java.util.List;
2426
import java.util.Locale;
2527
import java.util.Map;
@@ -78,8 +80,11 @@ public void doFilter(HttpExchange exchange, Chain chain) throws IOException {
7880
ScopedValue.where(DispatchHandler.CURRENT, request)
7981
.call(
8082
() -> {
81-
runInnerChain(exchange, chain);
82-
fireAfterHooks(exchange, request);
83+
try {
84+
runInnerChain(exchange, chain);
85+
} finally {
86+
fireAfterHooks(exchange, request);
87+
}
8388
return null;
8489
});
8590
} catch (IOException | RuntimeException e) {
@@ -132,7 +137,42 @@ private void runInnerChain(HttpExchange exchange, Chain chain) throws IOExceptio
132137
}
133138

134139
private void fireAfterHooks(HttpExchange exchange, Request request) {
135-
// implemented in Task 5
140+
Response response = resolveResponse(exchange);
141+
List<Runnable> snapshot = List.copyOf(request.afterHooks());
142+
143+
for (AfterResponseHook hook : afterHooks) {
144+
try {
145+
hook.after(request, response);
146+
} catch (Throwable t) {
147+
LOG.debug("after-response hook threw", t);
148+
}
149+
}
150+
for (Runnable runnable : snapshot) {
151+
try {
152+
runnable.run();
153+
} catch (Throwable t) {
154+
LOG.debug("after-response runnable threw", t);
155+
}
156+
}
157+
}
158+
159+
private static Response resolveResponse(HttpExchange exchange) {
160+
Object stashed = exchange.getAttribute(DispatchHandler.RESPONSE_ATTR);
161+
if (stashed instanceof Response r) {
162+
return r;
163+
}
164+
Headers headers = exchange.getResponseHeaders();
165+
String contentType = headers != null ? headers.getFirst("Content-Type") : null;
166+
Map<String, String> flat = new LinkedHashMap<>();
167+
if (headers != null) {
168+
for (Map.Entry<String, List<String>> e : headers.entrySet()) {
169+
List<String> values = e.getValue();
170+
if (values != null && !values.isEmpty()) {
171+
flat.put(e.getKey(), values.get(0));
172+
}
173+
}
174+
}
175+
return new Response(exchange.getResponseCode(), null, contentType, flat);
136176
}
137177

138178
private String stripBasePath(String path) {

0 commit comments

Comments
 (0)