Skip to content

Commit bc503f7

Browse files
committed
refactor: Fold exception handling into RequestPreparationFilter
Move inner-chain exception catching from ExceptionFilter into RequestPreparationFilter so after-hooks can fire inside the still-bound ScopedValue block (Task 4 of 9 — behavior-preserving). ExceptionFilter is kept and still wires onto extra-route contexts.
1 parent 25c0eba commit bc503f7

3 files changed

Lines changed: 92 additions & 25 deletions

File tree

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,16 @@ record HandlerConfig(
9595

9696
String basePath = Optional.ofNullable(spec.basePath()).orElse("/");
9797
HttpContext ctx = httpServer.createContext(basePath);
98-
ctx.getFilters().add(new ExceptionFilter(exceptionHandler, renderer));
99-
ctx.getFilters().add(new RequestPreparationFilter(spec, router, validator, bodyMappers));
98+
ctx.getFilters()
99+
.add(
100+
new RequestPreparationFilter(
101+
spec,
102+
router,
103+
validator,
104+
bodyMappers,
105+
exceptionHandler,
106+
renderer,
107+
handlerConfig.afterHooks()));
100108
ctx.getFilters()
101109
.add(
102110
new SecurityFilter(

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

Lines changed: 66 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package com.retailsvc.http.internal;
22

3+
import com.retailsvc.http.AfterResponseHook;
4+
import com.retailsvc.http.ExceptionHandler;
35
import com.retailsvc.http.MethodNotAllowedException;
46
import com.retailsvc.http.NotFoundException;
57
import com.retailsvc.http.Request;
8+
import com.retailsvc.http.Response;
69
import com.retailsvc.http.TypeMapper;
710
import com.retailsvc.http.ValidationException;
811
import com.retailsvc.http.spec.HttpMethod;
@@ -17,25 +20,42 @@
1720
import com.sun.net.httpserver.HttpExchange;
1821
import java.io.IOException;
1922
import java.util.HashMap;
23+
import java.util.List;
2024
import java.util.Locale;
2125
import java.util.Map;
2226
import java.util.Optional;
27+
import org.slf4j.Logger;
28+
import org.slf4j.LoggerFactory;
2329

2430
public final class RequestPreparationFilter extends Filter {
2531

32+
private static final Logger LOG = LoggerFactory.getLogger(RequestPreparationFilter.class);
2633
private static final String BODY_POINTER = "/body";
2734

2835
private final Spec spec;
2936
private final Router router;
3037
private final Validator validator;
3138
private final Map<String, TypeMapper> bodyMappers;
39+
private final ExceptionHandler exceptionHandler;
40+
private final ResponseRenderer renderer;
41+
private final List<AfterResponseHook> afterHooks;
3242

43+
@SuppressWarnings("java:S107")
3344
public RequestPreparationFilter(
34-
Spec spec, Router router, Validator validator, Map<String, TypeMapper> bodyMappers) {
45+
Spec spec,
46+
Router router,
47+
Validator validator,
48+
Map<String, TypeMapper> bodyMappers,
49+
ExceptionHandler exceptionHandler,
50+
ResponseRenderer renderer,
51+
List<AfterResponseHook> afterHooks) {
3552
this.spec = spec;
3653
this.router = router;
3754
this.validator = validator;
3855
this.bodyMappers = Map.copyOf(bodyMappers);
56+
this.exceptionHandler = exceptionHandler;
57+
this.renderer = renderer;
58+
this.afterHooks = List.copyOf(afterHooks);
3959
}
4060

4161
@Override
@@ -45,6 +65,31 @@ public String description() {
4565

4666
@Override
4767
public void doFilter(HttpExchange exchange, Chain chain) throws IOException {
68+
Request request;
69+
try {
70+
request = buildRequest(exchange);
71+
} catch (RuntimeException | IOException t) {
72+
Response response = exceptionHandler.handle(t);
73+
renderer.render(exchange, response);
74+
return;
75+
}
76+
77+
try {
78+
ScopedValue.where(DispatchHandler.CURRENT, request)
79+
.call(
80+
() -> {
81+
runInnerChain(exchange, chain);
82+
fireAfterHooks(exchange, request);
83+
return null;
84+
});
85+
} catch (IOException | RuntimeException e) {
86+
throw e;
87+
} catch (Exception e) {
88+
throw new IOException(e);
89+
}
90+
}
91+
92+
private Request buildRequest(HttpExchange exchange) throws IOException {
4893
byte[] body = exchange.getRequestBody().readAllBytes();
4994

5095
HttpMethod method = HttpMethod.parse(exchange.getRequestMethod());
@@ -65,32 +110,31 @@ public void doFilter(HttpExchange exchange, Chain chain) throws IOException {
65110
ParsedBody parsedBody = validateAndParseBody(exchange, op, body);
66111

67112
var headers = exchange.getRequestHeaders();
68-
Request request =
69-
new Request(
70-
body,
71-
parsedBody.value(),
72-
parsedBody.mapper(),
73-
op.operationId(),
74-
match.pathParameters(),
75-
exchange.getRequestURI().getRawQuery(),
76-
headers::getFirst,
77-
Map.of(),
78-
method);
113+
return new Request(
114+
body,
115+
parsedBody.value(),
116+
parsedBody.mapper(),
117+
op.operationId(),
118+
match.pathParameters(),
119+
exchange.getRequestURI().getRawQuery(),
120+
headers::getFirst,
121+
Map.of(),
122+
method);
123+
}
79124

125+
private void runInnerChain(HttpExchange exchange, Chain chain) throws IOException {
80126
try {
81-
ScopedValue.where(DispatchHandler.CURRENT, request)
82-
.call(
83-
() -> {
84-
chain.doFilter(exchange);
85-
return null;
86-
});
87-
} catch (IOException | RuntimeException e) {
88-
throw e;
89-
} catch (Exception e) {
90-
throw new IOException(e);
127+
chain.doFilter(exchange);
128+
} catch (RuntimeException | IOException t) {
129+
Response response = exceptionHandler.handle(t);
130+
renderer.render(exchange, response);
91131
}
92132
}
93133

134+
private void fireAfterHooks(HttpExchange exchange, Request request) {
135+
// implemented in Task 5
136+
}
137+
94138
private String stripBasePath(String path) {
95139
String base = spec.basePath();
96140
if (base == null || base.isEmpty() || base.equals("/")) {

src/test/java/com/retailsvc/http/internal/RequestPreparationFilterTest.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import static org.assertj.core.api.Assertions.assertThatThrownBy;
55
import static org.mockito.Mockito.mock;
66

7+
import com.retailsvc.http.ExceptionHandler;
78
import com.retailsvc.http.MethodNotAllowedException;
89
import com.retailsvc.http.NotFoundException;
910
import com.retailsvc.http.Request;
@@ -26,6 +27,7 @@
2627
import com.sun.net.httpserver.Headers;
2728
import com.sun.net.httpserver.HttpExchange;
2829
import java.io.ByteArrayInputStream;
30+
import java.io.IOException;
2931
import java.net.URI;
3032
import java.nio.charset.StandardCharsets;
3133
import java.util.List;
@@ -77,8 +79,21 @@ public byte[] writeTo(Object value) {
7779
}
7880
};
7981
Map<String, TypeMapper> mappers = Map.of("application/json", textMapper);
82+
ExceptionHandler rethrow =
83+
t -> {
84+
if (t instanceof RuntimeException re) {
85+
throw re;
86+
}
87+
throw new IllegalStateException(t);
88+
};
8089
return new RequestPreparationFilter(
81-
spec, new Router(spec.operations()), new DefaultValidator(spec::resolveSchema), mappers);
90+
spec,
91+
new Router(spec.operations()),
92+
new DefaultValidator(spec::resolveSchema),
93+
mappers,
94+
rethrow,
95+
new ResponseRenderer(mappers),
96+
List.of());
8297
}
8398

8499
@Test

0 commit comments

Comments
 (0)