Skip to content

Commit 78ed3a4

Browse files
committed
feat: Support configurable bind address
1 parent 476f625 commit 78ed3a4

3 files changed

Lines changed: 95 additions & 3 deletions

File tree

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,21 @@ Register a custom mapper for any media type via `Builder.bodyMapper(mediaType, m
218218

219219
User-supplied mappers take precedence over built-in defaults, so you can override any of the above.
220220

221+
#### Restricting to the loopback interface
222+
223+
By default the server binds to the wildcard address (all local interfaces). To restrict it to loopback — useful for local development or sidecar processes — supply a bind address:
224+
225+
```java
226+
import java.net.InetAddress;
227+
228+
OpenApiServer.builder()
229+
.spec(spec)
230+
.handlers(handlers)
231+
.port(8080)
232+
.bindAddress(InetAddress.getLoopbackAddress())
233+
.build();
234+
```
235+
221236
### Response decorators
222237

223238
`Builder.responseDecorator(...)` registers a `ResponseDecorator` — a `(Request, Response) -> Response` transform applied to every handler's return value before rendering. Decorators compose in registration order: the result of one is fed to the next. Decorator-supplied headers override handler-supplied ones; if you want the opposite, set the header inside the handler with `Response.withHeader(...)`.

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

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.sun.net.httpserver.HttpHandler;
2222
import com.sun.net.httpserver.HttpServer;
2323
import java.io.IOException;
24+
import java.net.InetAddress;
2425
import java.net.InetSocketAddress;
2526
import java.util.ArrayList;
2627
import java.util.LinkedHashMap;
@@ -65,6 +66,7 @@ record HandlerConfig(
6566
Map<String, TypeMapper> bodyMappers,
6667
HandlerConfig handlerConfig,
6768
int port,
69+
InetAddress bindAddress,
6870
int shutdownTimeoutSeconds)
6971
throws IOException {
7072

@@ -84,7 +86,11 @@ record HandlerConfig(
8486
.collect(Collectors.toUnmodifiableMap(Operation::operationId, op -> op));
8587
DefaultValidator validator = new DefaultValidator(spec::resolveSchema);
8688

87-
this.httpServer = HttpServer.create(new InetSocketAddress(port), 0);
89+
InetSocketAddress socketAddress =
90+
(bindAddress == null)
91+
? new InetSocketAddress(port)
92+
: new InetSocketAddress(bindAddress, port);
93+
this.httpServer = HttpServer.create(socketAddress, 0);
8894
httpServer.setExecutor(newThreadPerTaskExecutor(ofVirtual().name("http-", 0).factory()));
8995

9096
HttpContext ctx = httpServer.createContext(Optional.ofNullable(spec.basePath()).orElse("/"));
@@ -116,13 +122,25 @@ record HandlerConfig(
116122

117123
this.shutdownTimeoutSeconds = shutdownTimeoutSeconds;
118124

119-
LOG.info("Server started (port {}) in {}ms", port, System.currentTimeMillis() - t0);
125+
LOG.info(
126+
"Server started ({}:{}) in {}ms",
127+
httpServer.getAddress().getHostString(),
128+
httpServer.getAddress().getPort(),
129+
System.currentTimeMillis() - t0);
120130
}
121131

122132
public int listenPort() {
123133
return httpServer.getAddress().getPort();
124134
}
125135

136+
/**
137+
* Returns the actual address the server is bound to, including any wildcard resolution by the
138+
* underlying {@link HttpServer}. Useful for verifying loopback restriction.
139+
*/
140+
public InetAddress bindAddress() {
141+
return httpServer.getAddress().getAddress();
142+
}
143+
126144
/**
127145
* Stops the server, waiting up to {@code delaySeconds} for active exchanges to finish before
128146
* closing them. {@code 0} stops immediately.
@@ -157,6 +175,7 @@ public static final class Builder {
157175
private final List<RequestInterceptor> interceptors = new ArrayList<>();
158176
private ExceptionHandler exceptionHandler;
159177
private int port = DEFAULT_PORT;
178+
private InetAddress bindAddress;
160179
private int shutdownTimeoutSeconds = 0;
161180
private final LinkedHashMap<String, HttpHandler> extras = new LinkedHashMap<>();
162181
private final Map<String, SchemeValidator> securityValidators = new LinkedHashMap<>();
@@ -238,6 +257,16 @@ public Builder port(int port) {
238257
return this;
239258
}
240259

260+
/**
261+
* Restricts the server to a specific local interface. {@code null} (the default) binds to the
262+
* wildcard address (all interfaces). Use {@link InetAddress#getLoopbackAddress()} to listen on
263+
* loopback only.
264+
*/
265+
public Builder bindAddress(InetAddress bindAddress) {
266+
this.bindAddress = bindAddress;
267+
return this;
268+
}
269+
241270
/**
242271
* Sets the default drain timeout used by {@link OpenApiServer#close()}. {@code 0} (the default)
243272
* stops immediately; positive values wait up to that many seconds for in-flight exchanges to
@@ -291,7 +320,8 @@ public OpenApiServer build() throws IOException {
291320
extras,
292321
Map.copyOf(securityValidators),
293322
externalAuth);
294-
return new OpenApiServer(spec, resolved, handlerConfig, port, shutdownTimeoutSeconds);
323+
return new OpenApiServer(
324+
spec, resolved, handlerConfig, port, bindAddress, shutdownTimeoutSeconds);
295325
}
296326

297327
private static void validateSecurityWiring(Spec spec, Map<String, SchemeValidator> validators) {

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

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

33
import static java.util.Collections.emptyMap;
4+
import static org.assertj.core.api.Assertions.assertThat;
45
import static org.assertj.core.api.Assertions.assertThatThrownBy;
56
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
67

78
import com.retailsvc.http.spec.Spec;
9+
import java.io.IOException;
10+
import java.net.HttpURLConnection;
11+
import java.net.InetAddress;
12+
import java.net.URI;
813
import java.util.List;
914
import java.util.Map;
1015
import org.junit.jupiter.api.Test;
@@ -53,6 +58,48 @@ void testExceptionIsThrownOnInvalidHttpPort() {
5358
assertThatThrownBy(builder::build).isInstanceOf(IllegalArgumentException.class);
5459
}
5560

61+
@Test
62+
void shouldBindOnlyToLoopbackWhenBindAddressIsLoopback() throws IOException {
63+
try (var server =
64+
OpenApiServer.builder()
65+
.spec(testSpec())
66+
.handlers(emptyMap())
67+
.port(0)
68+
.bindAddress(InetAddress.getLoopbackAddress())
69+
.build()) {
70+
int port = server.listenPort();
71+
HttpURLConnection conn =
72+
(HttpURLConnection)
73+
URI.create("http://127.0.0.1:" + port + "/api/missing").toURL().openConnection();
74+
try {
75+
assertThat(conn.getResponseCode()).isEqualTo(HttpURLConnection.HTTP_NOT_FOUND);
76+
} finally {
77+
conn.disconnect();
78+
}
79+
}
80+
}
81+
82+
@Test
83+
void shouldBindToWildcardWhenBindAddressIsUnset() throws IOException {
84+
try (var server =
85+
OpenApiServer.builder().spec(testSpec()).handlers(emptyMap()).port(0).build()) {
86+
assertThat(server.bindAddress().isAnyLocalAddress()).isTrue();
87+
}
88+
}
89+
90+
@Test
91+
void shouldBindToWildcardWhenBindAddressIsExplicitlyNull() throws IOException {
92+
try (var server =
93+
OpenApiServer.builder()
94+
.spec(testSpec())
95+
.handlers(emptyMap())
96+
.port(0)
97+
.bindAddress(null)
98+
.build()) {
99+
assertThat(server.bindAddress().isAnyLocalAddress()).isTrue();
100+
}
101+
}
102+
56103
private Spec testSpec() {
57104
Map<String, Object> raw =
58105
Map.of(

0 commit comments

Comments
 (0)