Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Changelog

## 0.1.1 — 2025-09-13

### Highlights
- Fixes “URI with undefined scheme” when `phoenixd.base_url` lacks an HTTP scheme by defaulting to `http://`.
- Centralizes configuration via shared `Configuration` util (env vars take precedence over `app.properties`).

### Changes
- fix: normalize base_url scheme and use shared Configuration
- F:phoenixd-rest/src/main/java/xyz/tcheeric/phoenixd/operation/AbstractOperation.java
- F:phoenixd-rest/src/test/java/xyz/tcheeric/phoenixd/operation/BaseUrlNormalizationTest.java
- F:phoenixd-test/src/test/java/xyz/tcheeric/phoenixd/test/TestUtils.java
- docs: document scheme defaulting for `phoenixd.base_url`
- F:docs/reference/configuration.md
- build: bump version to 0.1.1
- F:pom.xml
- F:phoenixd-rest/pom.xml
- F:phoenixd-test/pom.xml
- F:phoenixd-base/pom.xml
- F:phoenixd-model/pom.xml
- F:phoenixd-mock/pom.xml

### Behavior Notes
- `phoenixd.base_url` without a scheme now resolves as `http://...`.
- If `phoenixd.base_url` is unset or blank, an `IllegalArgumentException` is thrown during request construction.

### Configuration
- Env vars (preferred): `PHOENIXD_USERNAME`, `PHOENIXD_PASSWORD`, `PHOENIXD_BASE_URL`, `PHOENIXD_TIMEOUT`.
- Or `app.properties` on the classpath with `phoenixd.username`, `phoenixd.password`, `phoenixd.base_url`, `phoenixd.timeout`.

### Testing
- Command: `mvn -q verify`
- 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.

### API Changes
- None.

### Security
- No new dependencies; no changes to security-sensitive logic.

### Migration
- No breaking changes. If you relied on rejecting schemeless base URLs, be aware they now default to `http://`.

2 changes: 1 addition & 1 deletion docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Configuration can also be supplied through an `app.properties` file on the class
|-----|--------|---------|-------|
| `phoenixd.username` | String | empty | Username for basic authentication. |
| `phoenixd.password` | String | empty | Password for basic authentication. Store securely. |
| `phoenixd.base_url` | URL | empty | Base address of the API endpoint. |
| `phoenixd.base_url` | URL | empty | Base address of the API endpoint. If no scheme is provided, `http://` is assumed. |
| `phoenixd.timeout` | Integer (ms) | `5000` | HTTP request timeout. |
| `phoenixd.webhook_secret` | String | empty | Optional secret used to validate webhook callbacks. Treat as a secret. |

Expand Down
2 changes: 1 addition & 1 deletion phoenixd-base/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<groupId>xyz.tcheeric</groupId>
<artifactId>phoenixd-java</artifactId>
<version>0.1.0</version>
<version>0.1.1</version>
</parent>

<artifactId>phoenixd-base</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion phoenixd-mock/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<parent>
<groupId>xyz.tcheeric</groupId>
<artifactId>phoenixd-java</artifactId>
<version>0.1.0</version>
<version>0.1.1</version>
</parent>

<artifactId>phoenixd-mock</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion phoenixd-model/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<groupId>xyz.tcheeric</groupId>
<artifactId>phoenixd-java</artifactId>
<version>0.1.0</version>
<version>0.1.1</version>
</parent>

<artifactId>phoenixd-model</artifactId>
Expand Down
4 changes: 2 additions & 2 deletions phoenixd-rest/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
<parent>
<groupId>xyz.tcheeric</groupId>
<artifactId>phoenixd-java</artifactId>
<version>0.1.0</version>
<version>0.1.1</version>
</parent>

<artifactId>phoenixd-rest</artifactId>
<packaging>jar</packaging>
<version>0.1.0</version>
<version>0.1.1</version>
<name>phoenixd-rest</name>
<url>https://maven.apache.org</url>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import xyz.tcheeric.phoenixd.operation.impl.PostOperation;
import xyz.tcheeric.phoenixd.common.rest.Operation;
import xyz.tcheeric.phoenixd.common.rest.Request;
import xyz.tcheeric.phoenixd.common.rest.util.Configuration;

import java.io.IOException;
import java.net.URI;
Expand All @@ -19,7 +20,6 @@
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand All @@ -32,34 +32,34 @@ public abstract class AbstractOperation implements Operation {
private String requestData;

public static final long DEFAULT_TIMEOUT = 5000L;
private static final String PREFIX = "phoenixd.";
private static final Properties CONFIG = loadConfig();

@SneakyThrows
private static Properties loadConfig() {
Properties props = new Properties();
try (var stream = AbstractOperation.class.getResourceAsStream("/app.properties")) {
if (stream != null) {
props.load(stream);
}
}
return props;
}
private static final String PREFIX = "phoenixd";
private static final Configuration CONFIG = new Configuration(PREFIX);

private static String getProperty(String key) {
return CONFIG.getProperty(PREFIX + key);
return CONFIG.get(key);
}

private static long getLongProperty(String key, long defaultValue) {
String value = CONFIG.getProperty(PREFIX + key);
if (value == null) {
return defaultValue;
Long value = CONFIG.getLong(key, defaultValue);
return value != null ? value : defaultValue;
}

private static String ensureScheme(String baseUrl) {
if (baseUrl == null || baseUrl.isBlank()) {
throw new IllegalArgumentException("phoenixd.base_url is not set");
}
try {
return Long.parseLong(value);
} catch (NumberFormatException e) {
return defaultValue;
String trimmed = baseUrl.trim();
String lower = trimmed.toLowerCase();
if (lower.startsWith("http://") || lower.startsWith("https://")) {
return trimmed;
}
return "http://" + trimmed;
}

private static URI buildUri(String baseUrl, String path) {
String normalizedBase = ensureScheme(baseUrl);
String normalizedPath = (path == null || path.isEmpty()) ? "/" : (path.startsWith("/") ? path : "/" + path);
return URI.create(normalizedBase).resolve(normalizedPath);
Comment on lines +59 to +62

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve base URL paths when resolving request URIs

The new buildUri always prefixes the supplied path with / and feeds it to URI.create(normalizedBase).resolve(normalizedPath). For base URLs that include a non-root path like https://example.com/api, resolve("/items") discards the /api prefix and produces https://example.com/items, whereas the previous baseUrl + path concatenation correctly yielded https://example.com/api/items. Deployments that rely on a path segment in phoenixd.base_url will now send every request to the wrong endpoint. Consider resolving a relative path (without the leading slash) or concatenating after normalizing the scheme so the base path is preserved.

Useful? React with 👍 / 👎.

}

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

this.httpRequest = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + path))
.uri(buildUri(baseUrl, path))
.header("Authorization", "Basic " + encodedAuth)
.timeout(Duration.ofMillis(timeout))
.method(method, bodyPublisher)
Expand All @@ -107,7 +107,7 @@ public AbstractOperation(@NonNull String method, @NonNull String path, @NonNull
}

this.httpRequest = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + resolvedPath))
.uri(buildUri(baseUrl, resolvedPath))
.header("Authorization", "Basic " + encodedAuth)
.timeout(Duration.ofMillis(timeout))
.method(method, bodyPublisher)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package xyz.tcheeric.phoenixd.operation;

import org.junit.jupiter.api.Test;
import xyz.tcheeric.phoenixd.common.rest.util.Configuration;
import xyz.tcheeric.phoenixd.operation.impl.PostOperation;

import java.lang.reflect.Field;

import static org.assertj.core.api.Assertions.assertThat;

public class BaseUrlNormalizationTest {

// Verifies base_url without scheme defaults to http
@Test
void baseUrlWithoutSchemeDefaultsToHttp() throws Exception {
Field configField = AbstractOperation.class.getDeclaredField("CONFIG");
configField.setAccessible(true);
Configuration cfg = (Configuration) configField.get(null);
cfg.getProperties().setProperty("phoenixd.base_url", "localhost:9999");
cfg.getProperties().setProperty("phoenixd.username", "user");
cfg.getProperties().setProperty("phoenixd.password", "pass");

PostOperation op = new PostOperation("/items", "data");
assertThat(op.getHttpRequest().uri().getScheme()).isEqualTo("http");
assertThat(op.getHttpRequest().uri().toString()).isEqualTo("http://localhost:9999/items");
}
}

2 changes: 1 addition & 1 deletion phoenixd-test/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<groupId>xyz.tcheeric</groupId>
<artifactId>phoenixd-java</artifactId>
<version>0.1.0</version>
<version>0.1.1</version>
</parent>

<artifactId>phoenixd-test</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package xyz.tcheeric.phoenixd.test;

import xyz.tcheeric.phoenixd.operation.AbstractOperation;
import xyz.tcheeric.phoenixd.common.rest.util.Configuration;

import java.lang.reflect.Field;
import java.util.Properties;

public final class TestUtils {
private static final String CONFIG_FIELD_NAME = "CONFIG";
Expand All @@ -16,8 +16,14 @@ public static void setBaseUrl(String baseUrl) {
try {
Field configField = AbstractOperation.class.getDeclaredField(CONFIG_FIELD_NAME);
configField.setAccessible(true);
Properties props = (Properties) configField.get(null);
props.setProperty(PHOENIXD_BASE_URL_KEY, baseUrl);
Object cfg = configField.get(null);
if (cfg instanceof Configuration configuration) {
configuration.getProperties().setProperty(PHOENIXD_BASE_URL_KEY, baseUrl);
} else if (cfg instanceof java.util.Properties props) {
props.setProperty(PHOENIXD_BASE_URL_KEY, baseUrl);
} else {
throw new IllegalStateException("Unsupported CONFIG type: " + (cfg == null ? "null" : cfg.getClass()));
}
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>xyz.tcheeric</groupId>
<artifactId>phoenixd-java</artifactId>
<version>0.1.0</version>
<version>0.1.1</version>
<packaging>pom</packaging>

<modules>
Expand Down
Loading