Skip to content

Commit f124828

Browse files
committed
fix: Enforce one-terminal-per-Request across respond() calls
1 parent 11936f5 commit f124828

3 files changed

Lines changed: 25 additions & 2 deletions

File tree

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public final class Request {
1616
private final String operationId;
1717
private final Map<String, String> pathParameters;
1818
private final Map<String, TypeMapper> bodyMappers;
19+
private boolean responseSent;
1920

2021
public Request(
2122
HttpExchange exchange,
@@ -53,6 +54,9 @@ public String header(String name) {
5354
}
5455

5556
public ResponseBuilder respond(int status) {
56-
return new DefaultResponseBuilder(exchange, status, bodyMappers);
57+
if (responseSent) {
58+
throw new IllegalStateException("Response already sent");
59+
}
60+
return new DefaultResponseBuilder(exchange, status, bodyMappers, () -> responseSent = true);
5761
}
5862
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,16 @@ public final class DefaultResponseBuilder implements ResponseBuilder {
1717
private final HttpExchange exchange;
1818
private final int status;
1919
private final Map<String, TypeMapper> mappers;
20+
private final Runnable onTerminated;
2021
private final Map<String, String> pendingHeaders = new LinkedHashMap<>();
2122
private boolean terminated;
2223

2324
public DefaultResponseBuilder(
24-
HttpExchange exchange, int status, Map<String, TypeMapper> mappers) {
25+
HttpExchange exchange, int status, Map<String, TypeMapper> mappers, Runnable onTerminated) {
2526
this.exchange = exchange;
2627
this.status = status;
2728
this.mappers = mappers;
29+
this.onTerminated = onTerminated;
2830
}
2931

3032
@Override
@@ -125,6 +127,7 @@ public void close() throws IOException {
125127
private void terminate() {
126128
checkNotTerminated();
127129
terminated = true;
130+
onTerminated.run();
128131
}
129132

130133
private void checkNotTerminated() {

src/test/java/com/retailsvc/http/RequestTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package com.retailsvc.http;
22

33
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
45
import static org.mockito.Mockito.mock;
6+
import static org.mockito.Mockito.when;
57

68
import com.retailsvc.http.internal.DispatchHandler;
9+
import com.sun.net.httpserver.Headers;
710
import com.sun.net.httpserver.HttpExchange;
811
import java.util.Map;
912
import java.util.concurrent.atomic.AtomicReference;
@@ -44,4 +47,17 @@ void readsBoundContext() throws Exception {
4447
assertThat(seenOpId.get()).isEqualTo("get-x");
4548
assertThat(seenPathParams.get()).containsEntry("id", "42");
4649
}
50+
51+
@Test
52+
void respondAfterTerminalThrows() throws Exception {
53+
HttpExchange exchange = mock(HttpExchange.class);
54+
when(exchange.getResponseHeaders()).thenReturn(new Headers());
55+
Request req = new Request(exchange, new byte[0], null, "op", Map.of(), Map.of());
56+
57+
req.respond(204).empty();
58+
59+
assertThatThrownBy(() -> req.respond(200))
60+
.isInstanceOf(IllegalStateException.class)
61+
.hasMessageContaining("already sent");
62+
}
4763
}

0 commit comments

Comments
 (0)