diff --git a/ans-sdk-agent-client/src/main/java/com/godaddy/ans/sdk/agent/AnsVerifiedClient.java b/ans-sdk-agent-client/src/main/java/com/godaddy/ans/sdk/agent/AnsVerifiedClient.java index cd61b52..045ba22 100644 --- a/ans-sdk-agent-client/src/main/java/com/godaddy/ans/sdk/agent/AnsVerifiedClient.java +++ b/ans-sdk-agent-client/src/main/java/com/godaddy/ans/sdk/agent/AnsVerifiedClient.java @@ -345,8 +345,9 @@ public CompletableFuture connectAsync(String serverUrl) { .exceptionally(e -> { Throwable cause = e instanceof CompletionException && e.getCause() != null ? e.getCause() : e; - LOGGER.warn("SCITT preflight failed: {}", cause.getMessage()); - return ScittPreVerifyResult.parseError("Preflight failed: " + cause.getMessage()); + String detail = describe(cause); + LOGGER.warn("SCITT preflight to {}:{} failed: {}", hostname, port, detail, cause); + return ScittPreVerifyResult.parseError("Preflight failed: " + detail); }); } else { scittFuture = CompletableFuture.completedFuture(ScittPreVerifyResult.notPresent()); @@ -390,6 +391,15 @@ public void clearCapturedCertificates(String host) { }); } + /** + * Renders a throwable for diagnostics. Many TLS/IO exceptions carry a null message, which + * would otherwise surface as "null" and hide the failing class. Falls back to the class name. + */ + private static String describe(Throwable t) { + String message = t.getMessage(); + return message != null ? t.getClass().getName() + ": " + message : t.getClass().getName(); + } + private void assertScittResult(ScittPreVerifyResult scittPreResult, boolean scittVerified) { // Reject invalid SCITT headers regardless of mode (prevents garbage header attacks) if (policy.rejectsInvalidScittHeaders() && scittPreResult.isPresent() && !scittVerified) { diff --git a/ans-sdk-core/src/main/java/com/godaddy/ans/sdk/config/AnsConfiguration.java b/ans-sdk-core/src/main/java/com/godaddy/ans/sdk/config/AnsConfiguration.java index 6764baa..9b73e8b 100644 --- a/ans-sdk-core/src/main/java/com/godaddy/ans/sdk/config/AnsConfiguration.java +++ b/ans-sdk-core/src/main/java/com/godaddy/ans/sdk/config/AnsConfiguration.java @@ -206,8 +206,8 @@ public Builder enableRetry(int maxRetries) { * @throws NullPointerException if required fields are not set */ public AnsConfiguration build() { - if (this.environment == null) { - throw new IllegalStateException("Environment is required"); + if (this.environment == null && this.baseUrl == null) { + throw new IllegalStateException("Either an environment or a base URL is required"); } return new AnsConfiguration(this); } diff --git a/ans-sdk-core/src/test/java/com/godaddy/ans/sdk/config/AnsConfigurationTest.java b/ans-sdk-core/src/test/java/com/godaddy/ans/sdk/config/AnsConfigurationTest.java index ff14821..cc5c018 100644 --- a/ans-sdk-core/src/test/java/com/godaddy/ans/sdk/config/AnsConfigurationTest.java +++ b/ans-sdk-core/src/test/java/com/godaddy/ans/sdk/config/AnsConfigurationTest.java @@ -115,13 +115,25 @@ void shouldThrowExceptionWhenCredentialsProviderIsNull() { } @Test - @DisplayName("Should throw when environment is not set") - void shouldThrowWhenEnvironmentNotSet() { + @DisplayName("Should throw when neither environment nor base URL is set") + void shouldThrowWhenEnvironmentAndBaseUrlNotSet() { assertThatThrownBy(() -> AnsConfiguration.builder() .credentialsProvider(testProvider) .build()) .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("Environment is required"); + .hasMessageContaining("environment or a base URL"); + } + + @Test + @DisplayName("Should allow custom base URL without an environment") + void shouldAllowBaseUrlWithoutEnvironment() { + AnsConfiguration config = AnsConfiguration.builder() + .baseUrl("http://localhost:18080") + .credentialsProvider(testProvider) + .build(); + + assertThat(config.getBaseUrl()).isEqualTo("http://localhost:18080"); + assertThat(config.getEnvironment()).isNull(); } @Test