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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@ All notable changes to the **Sinch Java SDK** are documented in this file.

---

## v2.1 – unreleased

### SDK
- **[fix]** `HttpClientApache`: declare now `headersToBeAdded` as `volatile` to guarantee visibility across threads in concurrent usage
- **[fix]** `HttpClientApache`: wrap response-body `Scanner` in a try-with-resources block to prevent resource leaks; gracefully handle empty (`null`) response entities
- **[fix]** `SinchClient`: guard against a `NullPointerException` when `java.vendor` system property is absent while building the `User-Agent` auxiliary flag
- **[fix]** `Configuration`: correct copy-paste error in `toString()` and Javadoc — `conversationContext` label was incorrectly attributed to the Voice domain

### Examples / Snippets
- **[doc]** Fix typos in `conversation/conversations/Create` and `voice/applications/GetEventDestinations` snippets

---

## v2.0 – 2026-03-31

### Major breaking changes with major release
Expand Down
15 changes: 10 additions & 5 deletions client/src/main/com/sinch/sdk/SinchClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
import com.sinch.sdk.models.adapters.DualToneMultiFrequencyMapper;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import java.util.Properties;
import java.util.logging.Logger;
Expand Down Expand Up @@ -430,11 +430,16 @@ private String formatSdkUserAgentHeader() {
}

private String formatAuxiliaryFlag() {
return formatAuxiliaryFlag(SDK.AUXILIARY_FLAG);
}

Collection<String> values = Collections.singletonList(System.getProperty("java.vendor"));

if (!StringUtil.isEmpty(SDK.AUXILIARY_FLAG)) {
values.add(SDK.AUXILIARY_FLAG);
// Package-private to allow unit-testing with an arbitrary flag value
String formatAuxiliaryFlag(String auxiliaryFlag) {
Collection<String> values = new ArrayList<>();
String vendor = System.getProperty("java.vendor");
values.add(StringUtil.isEmpty(vendor) ? "" : vendor);
if (!StringUtil.isEmpty(auxiliaryFlag)) {
values.add(auxiliaryFlag);
}
return String.join(",", values);
}
Expand Down
15 changes: 10 additions & 5 deletions client/src/main/com/sinch/sdk/http/HttpClientApache.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public class HttpClientApache implements com.sinch.sdk.core.http.HttpClient {

private static final Logger LOGGER = Logger.getLogger(HttpClientApache.class.getName());

private Map<String, String> headersToBeAdded;
private volatile Map<String, String> headersToBeAdded;

private volatile CloseableHttpClient client;

Expand All @@ -71,10 +71,15 @@ private static HttpResponse processResponse(ClassicHttpResponse response) throws
}

HttpEntity entity = response.getEntity();
Scanner s = new Scanner(entity.getContent()).useDelimiter("\\A");
String content = (s.hasNext() ? s.next() : "");

return new HttpResponse(statusCode, message, headers, content.getBytes(StandardCharsets.UTF_8));
if (null == entity) {
return new HttpResponse(statusCode, message, headers, null);
}
try (Scanner s =
new Scanner(entity.getContent(), StandardCharsets.UTF_8.name()).useDelimiter("\\A")) {
String content = (s.hasNext() ? s.next() : "");
return new HttpResponse(
statusCode, message, headers, content.getBytes(StandardCharsets.UTF_8));
}
}

private static Map<String, List<String>> transformResponseHeaders(Header[] headers) {
Expand Down
6 changes: 2 additions & 4 deletions client/src/main/com/sinch/sdk/models/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ public String toString() {
+ verificationContext
+ ", voiceContext="
+ voiceContext
+ ", conversationRegion="
+ conversationContext
+ ", conversationContext="
+ conversationContext
+ "}";
Expand Down Expand Up @@ -153,9 +151,9 @@ public Optional<ApplicationCredentials> getApplicationCredentials() {
}

/**
* Get Voice domain related execution context
* Get Conversation domain related execution context
*
* @return Current Voice context
* @return Current Conversation context
* @since 1.0
*/
public Optional<ConversationContext> getConversationContext() {
Expand Down
39 changes: 39 additions & 0 deletions client/src/test/java/com/sinch/sdk/SinchClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -218,4 +218,43 @@ void voiceApplicationManagementUrlFromUrl() {
client.getConfiguration().getVoiceContext().get().getVoiceApplicationManagementUrl(),
"my foo url");
}

@Test
void formatAuxiliaryFlagReturnsVendorWhenFlagIsEmpty() {
SinchClient client = new SinchClient();
// AUXILIARY_FLAG is "" in SDK — only vendor should be present, no comma separator
String result = client.formatAuxiliaryFlag("");
assertNotNull(result);
assertEquals(
System.getProperty("java.vendor"),
result,
"When auxiliaryFlag is empty, the result should match java.vendor exactly");
}

@Test
void formatAuxiliaryFlagHandlesNullJavaVendor() {
String original = System.getProperty("java.vendor");
try {
System.clearProperty("java.vendor");
SinchClient client = new SinchClient();
String result = client.formatAuxiliaryFlag("");
assertNotNull(result);
assertFalse(result.contains("null"), "Null java.vendor must not render as 'null' string");
} finally {
if (null != original) {
System.setProperty("java.vendor", original);
}
}
}

@Test
void formatAuxiliaryFlagAppendsNonEmptyFlag() {
SinchClient client = new SinchClient();
String result = client.formatAuxiliaryFlag("my-wrapper/1.0");
assertNotNull(result);
assertTrue(result.contains(","), "A comma must separate vendor from the auxiliary flag");
assertTrue(
result.endsWith(",my-wrapper/1.0"),
"Auxiliary flag must be the last element after the comma");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,16 @@
import com.sinch.sdk.core.http.HttpRequest;
import com.sinch.sdk.core.http.HttpResponse;
import com.sinch.sdk.core.models.ServerConfiguration;
import java.io.ByteArrayInputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.Scanner;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpEntity;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -75,4 +82,32 @@ void testInvokeApiHandles401WithEmptyHeaders() throws Exception {
// AND: resetToken() is not called (no headers to check)
verify(mockAuthManager, never()).resetToken();
}

@Test
void processResponseDecodesBodyAsUtf8() throws Exception {
String nonAscii = "Ünïcödé resülts";
byte[] utf8Bytes = nonAscii.getBytes(StandardCharsets.UTF_8);

HttpEntity entity = mock(HttpEntity.class);
when(entity.getContent()).thenReturn(new ByteArrayInputStream(utf8Bytes));

ClassicHttpResponse httpResponse = mock(ClassicHttpResponse.class);
when(httpResponse.getCode()).thenReturn(200);
when(httpResponse.getReasonPhrase()).thenReturn("OK");
when(httpResponse.getHeaders()).thenReturn(new Header[0]);
when(httpResponse.getEntity()).thenReturn(entity);

Method processResponse =
HttpClientApache.class.getDeclaredMethod("processResponse", ClassicHttpResponse.class);
processResponse.setAccessible(true);

HttpResponse result = (HttpResponse) processResponse.invoke(null, httpResponse);

assertNotNull(result.getContent());
try (Scanner s =
new Scanner(result.getContent(), StandardCharsets.UTF_8.name()).useDelimiter("\\A")) {
String decoded = s.hasNext() ? s.next() : "";
assertEquals(nonAscii, decoded, "Response body must round-trip through UTF-8 correctly");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public static void main(String[] args) {

LOGGER.info(
String.format(
"Create conversation for applicatoin with ID '%s'", conversationApplicationId));
"Create conversation for application with ID '%s'", conversationApplicationId));

Conversation response = conversationsService.create(request);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public static void main(String[] args) {

ApplicationsService applicationsService = client.voice().v1().applications();

LOGGER.info(String.format("Get even destinations for application key '%s'", applicationKey));
LOGGER.info(String.format("Get event destinations for application key '%s'", applicationKey));

EventDestinations response = applicationsService.getEventDestinations(applicationKey);

Expand Down
Loading