Skip to content

Commit 6c02fde

Browse files
authored
Merge pull request #49 from 398ja/fix/base-url-scheme-0.1.1
fix/base-url-scheme-0.1.1
2 parents 48b5369 + e5a5aec commit 6c02fde

11 files changed

Lines changed: 113 additions & 35 deletions

File tree

CHANGELOG.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Changelog
2+
3+
## 0.1.1 — 2025-09-13
4+
5+
### Highlights
6+
- Fixes “URI with undefined scheme” when `phoenixd.base_url` lacks an HTTP scheme by defaulting to `http://`.
7+
- Centralizes configuration via shared `Configuration` util (env vars take precedence over `app.properties`).
8+
9+
### Changes
10+
- fix: normalize base_url scheme and use shared Configuration
11+
- F:phoenixd-rest/src/main/java/xyz/tcheeric/phoenixd/operation/AbstractOperation.java
12+
- F:phoenixd-rest/src/test/java/xyz/tcheeric/phoenixd/operation/BaseUrlNormalizationTest.java
13+
- F:phoenixd-test/src/test/java/xyz/tcheeric/phoenixd/test/TestUtils.java
14+
- docs: document scheme defaulting for `phoenixd.base_url`
15+
- F:docs/reference/configuration.md
16+
- build: bump version to 0.1.1
17+
- F:pom.xml
18+
- F:phoenixd-rest/pom.xml
19+
- F:phoenixd-test/pom.xml
20+
- F:phoenixd-base/pom.xml
21+
- F:phoenixd-model/pom.xml
22+
- F:phoenixd-mock/pom.xml
23+
24+
### Behavior Notes
25+
- `phoenixd.base_url` without a scheme now resolves as `http://...`.
26+
- If `phoenixd.base_url` is unset or blank, an `IllegalArgumentException` is thrown during request construction.
27+
28+
### Configuration
29+
- Env vars (preferred): `PHOENIXD_USERNAME`, `PHOENIXD_PASSWORD`, `PHOENIXD_BASE_URL`, `PHOENIXD_TIMEOUT`.
30+
- Or `app.properties` on the classpath with `phoenixd.username`, `phoenixd.password`, `phoenixd.base_url`, `phoenixd.timeout`.
31+
32+
### Testing
33+
- Command: `mvn -q verify`
34+
- Note: In restricted sandboxes, tests that open loopback sockets (MockWebServer) may fail with `SocketException: Operation not permitted`. In a normal dev/CI environment, the test suite is expected to pass.
35+
36+
### API Changes
37+
- None.
38+
39+
### Security
40+
- No new dependencies; no changes to security-sensitive logic.
41+
42+
### Migration
43+
- No breaking changes. If you relied on rejecting schemeless base URLs, be aware they now default to `http://`.
44+

docs/reference/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Configuration can also be supplied through an `app.properties` file on the class
2020
|-----|--------|---------|-------|
2121
| `phoenixd.username` | String | empty | Username for basic authentication. |
2222
| `phoenixd.password` | String | empty | Password for basic authentication. Store securely. |
23-
| `phoenixd.base_url` | URL | empty | Base address of the API endpoint. |
23+
| `phoenixd.base_url` | URL | empty | Base address of the API endpoint. If no scheme is provided, `http://` is assumed. |
2424
| `phoenixd.timeout` | Integer (ms) | `5000` | HTTP request timeout. |
2525
| `phoenixd.webhook_secret` | String | empty | Optional secret used to validate webhook callbacks. Treat as a secret. |
2626

phoenixd-base/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<parent>
55
<groupId>xyz.tcheeric</groupId>
66
<artifactId>phoenixd-java</artifactId>
7-
<version>0.1.0</version>
7+
<version>0.1.1</version>
88
</parent>
99

1010
<artifactId>phoenixd-base</artifactId>

phoenixd-mock/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<parent>
77
<groupId>xyz.tcheeric</groupId>
88
<artifactId>phoenixd-java</artifactId>
9-
<version>0.1.0</version>
9+
<version>0.1.1</version>
1010
</parent>
1111

1212
<artifactId>phoenixd-mock</artifactId>

phoenixd-model/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<parent>
55
<groupId>xyz.tcheeric</groupId>
66
<artifactId>phoenixd-java</artifactId>
7-
<version>0.1.0</version>
7+
<version>0.1.1</version>
88
</parent>
99

1010
<artifactId>phoenixd-model</artifactId>

phoenixd-rest/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
<parent>
55
<groupId>xyz.tcheeric</groupId>
66
<artifactId>phoenixd-java</artifactId>
7-
<version>0.1.0</version>
7+
<version>0.1.1</version>
88
</parent>
99

1010
<artifactId>phoenixd-rest</artifactId>
1111
<packaging>jar</packaging>
12-
<version>0.1.0</version>
12+
<version>0.1.1</version>
1313
<name>phoenixd-rest</name>
1414
<url>https://maven.apache.org</url>
1515

phoenixd-rest/src/main/java/xyz/tcheeric/phoenixd/operation/AbstractOperation.java

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import xyz.tcheeric.phoenixd.operation.impl.PostOperation;
77
import xyz.tcheeric.phoenixd.common.rest.Operation;
88
import xyz.tcheeric.phoenixd.common.rest.Request;
9+
import xyz.tcheeric.phoenixd.common.rest.util.Configuration;
910

1011
import java.io.IOException;
1112
import java.net.URI;
@@ -19,7 +20,6 @@
1920
import java.util.Base64;
2021
import java.util.List;
2122
import java.util.Map;
22-
import java.util.Properties;
2323
import java.util.concurrent.CompletableFuture;
2424
import java.util.stream.Collectors;
2525
import java.util.stream.Stream;
@@ -32,34 +32,34 @@ public abstract class AbstractOperation implements Operation {
3232
private String requestData;
3333

3434
public static final long DEFAULT_TIMEOUT = 5000L;
35-
private static final String PREFIX = "phoenixd.";
36-
private static final Properties CONFIG = loadConfig();
37-
38-
@SneakyThrows
39-
private static Properties loadConfig() {
40-
Properties props = new Properties();
41-
try (var stream = AbstractOperation.class.getResourceAsStream("/app.properties")) {
42-
if (stream != null) {
43-
props.load(stream);
44-
}
45-
}
46-
return props;
47-
}
35+
private static final String PREFIX = "phoenixd";
36+
private static final Configuration CONFIG = new Configuration(PREFIX);
4837

4938
private static String getProperty(String key) {
50-
return CONFIG.getProperty(PREFIX + key);
39+
return CONFIG.get(key);
5140
}
5241

5342
private static long getLongProperty(String key, long defaultValue) {
54-
String value = CONFIG.getProperty(PREFIX + key);
55-
if (value == null) {
56-
return defaultValue;
43+
Long value = CONFIG.getLong(key, defaultValue);
44+
return value != null ? value : defaultValue;
45+
}
46+
47+
private static String ensureScheme(String baseUrl) {
48+
if (baseUrl == null || baseUrl.isBlank()) {
49+
throw new IllegalArgumentException("phoenixd.base_url is not set");
5750
}
58-
try {
59-
return Long.parseLong(value);
60-
} catch (NumberFormatException e) {
61-
return defaultValue;
51+
String trimmed = baseUrl.trim();
52+
String lower = trimmed.toLowerCase();
53+
if (lower.startsWith("http://") || lower.startsWith("https://")) {
54+
return trimmed;
6255
}
56+
return "http://" + trimmed;
57+
}
58+
59+
private static URI buildUri(String baseUrl, String path) {
60+
String normalizedBase = ensureScheme(baseUrl);
61+
String normalizedPath = (path == null || path.isEmpty()) ? "/" : (path.startsWith("/") ? path : "/" + path);
62+
return URI.create(normalizedBase).resolve(normalizedPath);
6363
}
6464

6565
public AbstractOperation(@NonNull HttpRequest httpRequest) {
@@ -80,7 +80,7 @@ public AbstractOperation(@NonNull String method, @NonNull String path, String re
8080
HttpRequest.BodyPublisher bodyPublisher = requestData == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(requestData);
8181

8282
this.httpRequest = HttpRequest.newBuilder()
83-
.uri(URI.create(baseUrl + path))
83+
.uri(buildUri(baseUrl, path))
8484
.header("Authorization", "Basic " + encodedAuth)
8585
.timeout(Duration.ofMillis(timeout))
8686
.method(method, bodyPublisher)
@@ -107,7 +107,7 @@ public AbstractOperation(@NonNull String method, @NonNull String path, @NonNull
107107
}
108108

109109
this.httpRequest = HttpRequest.newBuilder()
110-
.uri(URI.create(baseUrl + resolvedPath))
110+
.uri(buildUri(baseUrl, resolvedPath))
111111
.header("Authorization", "Basic " + encodedAuth)
112112
.timeout(Duration.ofMillis(timeout))
113113
.method(method, bodyPublisher)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package xyz.tcheeric.phoenixd.operation;
2+
3+
import org.junit.jupiter.api.Test;
4+
import xyz.tcheeric.phoenixd.common.rest.util.Configuration;
5+
import xyz.tcheeric.phoenixd.operation.impl.PostOperation;
6+
7+
import java.lang.reflect.Field;
8+
9+
import static org.assertj.core.api.Assertions.assertThat;
10+
11+
public class BaseUrlNormalizationTest {
12+
13+
// Verifies base_url without scheme defaults to http
14+
@Test
15+
void baseUrlWithoutSchemeDefaultsToHttp() throws Exception {
16+
Field configField = AbstractOperation.class.getDeclaredField("CONFIG");
17+
configField.setAccessible(true);
18+
Configuration cfg = (Configuration) configField.get(null);
19+
cfg.getProperties().setProperty("phoenixd.base_url", "localhost:9999");
20+
cfg.getProperties().setProperty("phoenixd.username", "user");
21+
cfg.getProperties().setProperty("phoenixd.password", "pass");
22+
23+
PostOperation op = new PostOperation("/items", "data");
24+
assertThat(op.getHttpRequest().uri().getScheme()).isEqualTo("http");
25+
assertThat(op.getHttpRequest().uri().toString()).isEqualTo("http://localhost:9999/items");
26+
}
27+
}
28+

phoenixd-test/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<parent>
55
<groupId>xyz.tcheeric</groupId>
66
<artifactId>phoenixd-java</artifactId>
7-
<version>0.1.0</version>
7+
<version>0.1.1</version>
88
</parent>
99

1010
<artifactId>phoenixd-test</artifactId>

phoenixd-test/src/test/java/xyz/tcheeric/phoenixd/test/TestUtils.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package xyz.tcheeric.phoenixd.test;
22

33
import xyz.tcheeric.phoenixd.operation.AbstractOperation;
4+
import xyz.tcheeric.phoenixd.common.rest.util.Configuration;
45

56
import java.lang.reflect.Field;
6-
import java.util.Properties;
77

88
public final class TestUtils {
99
private static final String CONFIG_FIELD_NAME = "CONFIG";
@@ -16,8 +16,14 @@ public static void setBaseUrl(String baseUrl) {
1616
try {
1717
Field configField = AbstractOperation.class.getDeclaredField(CONFIG_FIELD_NAME);
1818
configField.setAccessible(true);
19-
Properties props = (Properties) configField.get(null);
20-
props.setProperty(PHOENIXD_BASE_URL_KEY, baseUrl);
19+
Object cfg = configField.get(null);
20+
if (cfg instanceof Configuration configuration) {
21+
configuration.getProperties().setProperty(PHOENIXD_BASE_URL_KEY, baseUrl);
22+
} else if (cfg instanceof java.util.Properties props) {
23+
props.setProperty(PHOENIXD_BASE_URL_KEY, baseUrl);
24+
} else {
25+
throw new IllegalStateException("Unsupported CONFIG type: " + (cfg == null ? "null" : cfg.getClass()));
26+
}
2127
} catch (ReflectiveOperationException e) {
2228
throw new RuntimeException(e);
2329
}

0 commit comments

Comments
 (0)