Skip to content

Commit ffb8af1

Browse files
authored
fix: Support loopback interface (#73)
1 parent 35ec1be commit ffb8af1

5 files changed

Lines changed: 485 additions & 3 deletions

File tree

README.md

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

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

242+
#### Listen port
243+
244+
`Builder.port(int)` is optional and defaults to `8080`. Pass `0` to bind on an ephemeral port and read the actual port back via `OpenApiServer.listenPort()` — useful for tests.
245+
246+
#### Restricting to the loopback interface
247+
248+
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:
249+
250+
```java
251+
import java.net.InetAddress;
252+
253+
OpenApiServer.builder()
254+
.spec(spec)
255+
.handlers(handlers)
256+
.port(8080)
257+
.bindAddress(InetAddress.getLoopbackAddress())
258+
.build();
259+
```
260+
242261
### Response decorators
243262

244263
`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(...)`.
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
# Configurable bind address — Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** Add an optional `Builder.bindAddress(InetAddress)` to `OpenApiServer` so callers can restrict the server to a specific local interface (e.g., loopback) instead of always binding to the wildcard address.
6+
7+
**Architecture:** Additive change. `Builder` gains an `InetAddress bindAddress` field (default `null`). The package-private constructor receives it as a new parameter and picks between `new InetSocketAddress(port)` (wildcard) and `new InetSocketAddress(bindAddress, port)`. Default path is byte-identical to current behavior.
8+
9+
**Tech Stack:** Java 25, JUnit 5, AssertJ, Mockito, JDK `com.sun.net.httpserver.HttpServer`.
10+
11+
---
12+
13+
## File Structure
14+
15+
- Modify: `src/main/java/com/retailsvc/http/OpenApiServer.java`
16+
- Add `bindAddress` field on `Builder`, builder method, constructor parameter, `InetSocketAddress` construction switch, startup log host:port format.
17+
- Modify: `src/test/java/com/retailsvc/http/OpenApiServerTest.java`
18+
- Add tests covering loopback binding, default wildcard binding, explicit-null behavior.
19+
- Modify: `README.md`
20+
- Add a short loopback-binding snippet to the Getting Started area.
21+
22+
No new files.
23+
24+
---
25+
26+
### Task 1: Failing test for loopback binding
27+
28+
**Files:**
29+
- Test: `src/test/java/com/retailsvc/http/OpenApiServerTest.java`
30+
31+
- [ ] **Step 1: Add imports and the failing test**
32+
33+
Add the following imports near the existing imports in `OpenApiServerTest.java`:
34+
35+
```java
36+
import static org.assertj.core.api.Assertions.assertThat;
37+
38+
import java.io.IOException;
39+
import java.net.HttpURLConnection;
40+
import java.net.InetAddress;
41+
import java.net.URI;
42+
```
43+
44+
Append this test method to `OpenApiServerTest`:
45+
46+
```java
47+
@Test
48+
void shouldBindOnlyToLoopbackWhenBindAddressIsLoopback() throws IOException {
49+
try (var server =
50+
OpenApiServer.builder()
51+
.spec(testSpec())
52+
.handlers(emptyMap())
53+
.port(0)
54+
.bindAddress(InetAddress.getLoopbackAddress())
55+
.build()) {
56+
int port = server.listenPort();
57+
HttpURLConnection conn =
58+
(HttpURLConnection) URI.create("http://127.0.0.1:" + port + "/api/missing").toURL().openConnection();
59+
try {
60+
assertThat(conn.getResponseCode()).isEqualTo(HttpURLConnection.HTTP_NOT_FOUND);
61+
} finally {
62+
conn.disconnect();
63+
}
64+
}
65+
}
66+
```
67+
68+
The handler map is empty and the path is unmapped — a 404 from the catch-all `/` context is sufficient to prove the server is listening on loopback.
69+
70+
- [ ] **Step 2: Run the test to verify it fails**
71+
72+
Run: `mvn test -Dtest=OpenApiServerTest#shouldBindOnlyToLoopbackWhenBindAddressIsLoopback`
73+
74+
Expected: FAIL (compilation error: `cannot find symbol: method bindAddress(InetAddress)`).
75+
76+
---
77+
78+
### Task 2: Add `bindAddress` to the builder and thread it through the constructor
79+
80+
**Files:**
81+
- Modify: `src/main/java/com/retailsvc/http/OpenApiServer.java`
82+
83+
- [ ] **Step 1: Add the `InetAddress` import**
84+
85+
Insert next to the existing `java.net.InetSocketAddress` import:
86+
87+
```java
88+
import java.net.InetAddress;
89+
```
90+
91+
- [ ] **Step 2: Add the constructor parameter and bind logic**
92+
93+
Change the constructor signature so `bindAddress` is threaded in as a new parameter (place it between `port` and `shutdownTimeoutSeconds`):
94+
95+
```java
96+
OpenApiServer(
97+
Spec spec,
98+
Map<String, TypeMapper> bodyMappers,
99+
HandlerConfig handlerConfig,
100+
int port,
101+
InetAddress bindAddress,
102+
int shutdownTimeoutSeconds)
103+
throws IOException {
104+
```
105+
106+
Replace the existing wildcard bind:
107+
108+
```java
109+
this.httpServer = HttpServer.create(new InetSocketAddress(port), 0);
110+
```
111+
112+
with:
113+
114+
```java
115+
InetSocketAddress socketAddress =
116+
(bindAddress == null) ? new InetSocketAddress(port) : new InetSocketAddress(bindAddress, port);
117+
this.httpServer = HttpServer.create(socketAddress, 0);
118+
```
119+
120+
Update the startup log line so the bound host is visible:
121+
122+
```java
123+
LOG.info(
124+
"Server started ({}:{}) in {}ms",
125+
httpServer.getAddress().getHostString(),
126+
httpServer.getAddress().getPort(),
127+
System.currentTimeMillis() - t0);
128+
```
129+
130+
- [ ] **Step 3: Add the builder field and method**
131+
132+
Inside `Builder`, add a field next to the existing `port` field:
133+
134+
```java
135+
private InetAddress bindAddress;
136+
```
137+
138+
Add the builder method (place it directly under `port(int)`):
139+
140+
```java
141+
/**
142+
* Restricts the server to a specific local interface. {@code null} (the default) binds to the
143+
* wildcard address (all interfaces). Use {@link InetAddress#getLoopbackAddress()} to listen on
144+
* loopback only.
145+
*/
146+
public Builder bindAddress(InetAddress bindAddress) {
147+
this.bindAddress = bindAddress;
148+
return this;
149+
}
150+
```
151+
152+
Update the `build()` call to the constructor to pass `bindAddress`:
153+
154+
```java
155+
return new OpenApiServer(spec, resolved, handlerConfig, port, bindAddress, shutdownTimeoutSeconds);
156+
```
157+
158+
- [ ] **Step 4: Run the loopback test — expect PASS**
159+
160+
Run: `mvn test -Dtest=OpenApiServerTest#shouldBindOnlyToLoopbackWhenBindAddressIsLoopback`
161+
162+
Expected: PASS.
163+
164+
- [ ] **Step 5: Run the full unit-test suite to confirm no regressions**
165+
166+
Run: `mvn test`
167+
168+
Expected: all tests pass.
169+
170+
---
171+
172+
### Task 3: Failing test for default wildcard binding
173+
174+
**Files:**
175+
- Test: `src/test/java/com/retailsvc/http/OpenApiServerTest.java`
176+
177+
- [ ] **Step 1: Add the failing test**
178+
179+
Append:
180+
181+
```java
182+
@Test
183+
void shouldBindToWildcardWhenBindAddressIsUnset() throws IOException {
184+
try (var server =
185+
OpenApiServer.builder().spec(testSpec()).handlers(emptyMap()).port(0).build()) {
186+
assertThat(server.bindAddress().isAnyLocalAddress()).isTrue();
187+
}
188+
}
189+
190+
@Test
191+
void shouldBindToWildcardWhenBindAddressIsExplicitlyNull() throws IOException {
192+
try (var server =
193+
OpenApiServer.builder()
194+
.spec(testSpec())
195+
.handlers(emptyMap())
196+
.port(0)
197+
.bindAddress(null)
198+
.build()) {
199+
assertThat(server.bindAddress().isAnyLocalAddress()).isTrue();
200+
}
201+
}
202+
```
203+
204+
These reference a yet-to-exist `bindAddress()` accessor on `OpenApiServer`.
205+
206+
- [ ] **Step 2: Run the new tests to verify they fail**
207+
208+
Run: `mvn test -Dtest=OpenApiServerTest#shouldBindToWildcardWhenBindAddressIsUnset+shouldBindToWildcardWhenBindAddressIsExplicitlyNull`
209+
210+
Expected: FAIL (compilation error: `cannot find symbol: method bindAddress()`).
211+
212+
---
213+
214+
### Task 4: Expose the bound address on `OpenApiServer`
215+
216+
**Files:**
217+
- Modify: `src/main/java/com/retailsvc/http/OpenApiServer.java`
218+
219+
- [ ] **Step 1: Add the accessor**
220+
221+
Add directly below the existing `listenPort()` method:
222+
223+
```java
224+
/**
225+
* Returns the actual address the server is bound to, including any wildcard resolution by the
226+
* underlying {@link HttpServer}. Useful for verifying loopback restriction.
227+
*/
228+
public InetAddress bindAddress() {
229+
return httpServer.getAddress().getAddress();
230+
}
231+
```
232+
233+
- [ ] **Step 2: Run the wildcard tests to verify they pass**
234+
235+
Run: `mvn test -Dtest=OpenApiServerTest#shouldBindToWildcardWhenBindAddressIsUnset+shouldBindToWildcardWhenBindAddressIsExplicitlyNull`
236+
237+
Expected: PASS.
238+
239+
- [ ] **Step 3: Run the full unit-test suite**
240+
241+
Run: `mvn test`
242+
243+
Expected: all tests pass.
244+
245+
---
246+
247+
### Task 5: README snippet
248+
249+
**Files:**
250+
- Modify: `README.md`
251+
252+
- [ ] **Step 1: Add a loopback example**
253+
254+
In the section that documents builder configuration (under "Getting Started" / "Basic Usage", near the `port` mention if any), add:
255+
256+
````markdown
257+
#### Restricting to the loopback interface
258+
259+
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:
260+
261+
```java
262+
import java.net.InetAddress;
263+
264+
OpenApiServer.builder()
265+
.spec(spec)
266+
.handlers(handlers)
267+
.port(8080)
268+
.bindAddress(InetAddress.getLoopbackAddress())
269+
.build();
270+
```
271+
````
272+
273+
- [ ] **Step 2: Commit the full change**
274+
275+
```bash
276+
git add src/main/java/com/retailsvc/http/OpenApiServer.java \
277+
src/test/java/com/retailsvc/http/OpenApiServerTest.java \
278+
README.md
279+
SKIP=commitlint git commit -m "feat: Support configurable bind address"
280+
```
281+
282+
---
283+
284+
### Task 6: Final verification
285+
286+
- [ ] **Step 1: Run the full verification suite**
287+
288+
Run: `mvn verify`
289+
290+
Expected: build succeeds, all unit and integration tests pass.
291+
292+
- [ ] **Step 2: Analyze touched files with SonarLint MCP**
293+
294+
Per project memory: scan `OpenApiServer.java`, `OpenApiServerTest.java`, and any other modified files. Fix any new issues in the same branch before pushing.
295+
296+
- [ ] **Step 3: Push the branch**
297+
298+
```bash
299+
git push -u origin fix/support-loopback
300+
```
301+
302+
Per project memory: `gh` cannot open PRs here — the user opens the PR manually after the branch is pushed.

0 commit comments

Comments
 (0)