Skip to content

Commit

Permalink
DEVEXP-536: Fix UTF-8 encoding for body payloads (#121)
Browse files Browse the repository at this point in the history
* fix (HttpClientApache): Defaulting to send UTF-8 encoded payload
  • Loading branch information
JPPortier authored Aug 19, 2024
1 parent 5b00bc1 commit 5a651b9
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 3 deletions.
22 changes: 19 additions & 3 deletions client/src/main/com/sinch/sdk/http/HttpClientApache.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.sinch.sdk.auth.adapters.OAuthManager;
import com.sinch.sdk.core.exceptions.ApiException;
import com.sinch.sdk.core.http.AuthManager;
import com.sinch.sdk.core.http.HttpContentType;
import com.sinch.sdk.core.http.HttpMethod;
import com.sinch.sdk.core.http.HttpRequest;
import com.sinch.sdk.core.http.HttpResponse;
Expand All @@ -16,9 +17,11 @@
import com.sinch.sdk.core.models.ServerConfiguration;
import com.sinch.sdk.core.utils.Pair;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -115,14 +118,14 @@ public HttpResponse invokeAPI(

setUri(requestBuilder, path, queryParameters);

addBody(requestBuilder, body);

addCollectionHeader(requestBuilder, CONTENT_TYPE_HEADER, contentType);
addCollectionHeader(requestBuilder, "Accept", accept);

addHeaders(requestBuilder, headerParams);
addHeaders(requestBuilder, headersToBeAdded);

addBody(requestBuilder, body);

addAuth(requestBuilder, authManagersByOasSecuritySchemes, authNames, body);

ClassicHttpRequest request = requestBuilder.build();
Expand Down Expand Up @@ -204,8 +207,10 @@ private void setUri(
}

private void addBody(ClassicRequestBuilder requestBuilder, String body) {

if (null != body) {
requestBuilder.setEntity(new StringEntity(body));
Charset charset = extractCharset(requestBuilder).orElse(StandardCharsets.UTF_8);
requestBuilder.setEntity(new StringEntity(body, charset));
}
}

Expand Down Expand Up @@ -263,6 +268,17 @@ private HttpResponse processRequest(CloseableHttpClient client, ClassicHttpReque
return client.execute(request, HttpClientApache::processResponse);
}

private Optional<Charset> extractCharset(ClassicRequestBuilder requestBuilder) {

Optional<Header> charsetHeader =
Arrays.stream(requestBuilder.getHeaders(CONTENT_TYPE_HEADER))
.filter(f -> f.getValue().contains("charset="))
.findFirst();

return charsetHeader.flatMap(
header -> HttpContentType.getCharsetValue(header.getValue()).map(Charset::forName));
}

@Override
public boolean isClosed() {
return null == client;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
import com.sinch.sdk.http.HttpClientApache;
import com.sinch.sdk.models.UnifiedCredentials;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.AbstractMap;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -174,6 +176,35 @@ void bearerAutoRefresh() throws InterruptedException {
assertEquals(header, "Bearer another token");
}

@Test
void httpRequestBody() throws InterruptedException {

String sdkBody = "my body with Unicode characters: ✉";

mockBackEnd.enqueue(
new MockResponse().setBody("foo").addHeader("Content-Type", "application/json"));

try {
httpClient.invokeAPI(
new ServerConfiguration(String.format("%s/foo/", serverUrl)),
null,
new HttpRequest(
"foo-path",
HttpMethod.POST,
null,
sdkBody,
null,
null,
Arrays.asList("application/json; charset=utf-8"),
null));
} catch (ApiException ae) {
// noop
}
RecordedRequest recordedRequest = mockBackEnd.takeRequest();
String payloadBody = recordedRequest.getBody().readString(StandardCharsets.UTF_8);
assertEquals(payloadBody, sdkBody);
}

@Test
void httpRequestHeaders() throws InterruptedException {
String key = "My-OAS-Header-Key";
Expand Down
18 changes: 18 additions & 0 deletions core/src/main/com/sinch/sdk/core/http/HttpContentType.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
package com.sinch.sdk.core.http;

import com.sinch.sdk.core.utils.StringUtil;
import java.util.Collection;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class HttpContentType {

public static final String CONTENT_TYPE_HEADER = "content-type";
public static final String APPLICATION_JSON = "application/json";
public static final String TEXT_PLAIN = "text/plain";

static Pattern charsetPattern = Pattern.compile("(.*;$)?\\s*?charset=\\s*([^;\\s]+)");

public static boolean isMimeJson(Collection<String> mimes) {
String jsonMime = "(?i)^(" + APPLICATION_JSON + "|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$";
return mimes.stream()
Expand All @@ -17,4 +23,16 @@ public static boolean isMimeJson(Collection<String> mimes) {
public static boolean isMimeTextPlain(Collection<String> mimes) {
return mimes.stream().anyMatch(TEXT_PLAIN::equalsIgnoreCase);
}

public static Optional<String> getCharsetValue(String contentTypeHeader) {
if (StringUtil.isEmpty(contentTypeHeader)) {
return Optional.empty();
}

Matcher m = charsetPattern.matcher(contentTypeHeader);
if (!m.find()) {
return Optional.empty();
}
return Optional.of(m.group(2));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.sinch.sdk.core.http;

import java.util.Optional;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

class HttpContentTypeTest {

@Test
void getCharsetValueDefaultEmpty() {
Assertions.assertThat(HttpContentType.getCharsetValue("")).isEqualTo(Optional.empty());
}

@Test
void getCharsetValueNullEmpty() {
Assertions.assertThat(HttpContentType.getCharsetValue(null)).isEqualTo(Optional.empty());
}

@Test
void getCharsetValueNoCharset() {
Assertions.assertThat(HttpContentType.getCharsetValue("text/html")).isEqualTo(Optional.empty());
}

@Test
void getCharsetValueNoCharsetWithSemiColon() {
Assertions.assertThat(HttpContentType.getCharsetValue("text/html;"))
.isEqualTo(Optional.empty());
}

@Test
void getCharsetValueStartOfString() {
Assertions.assertThat(HttpContentType.getCharsetValue(" charset=utf-16; text/html"))
.isEqualTo(Optional.of("utf-16"));
}

@Test
void getCharsetValueStartOfStringWithSemiColon() {
Assertions.assertThat(HttpContentType.getCharsetValue(" charset=utf-16; text/html;"))
.isEqualTo(Optional.of("utf-16"));
}

@Test
void getCharsetValueEndOfString() {
Assertions.assertThat(HttpContentType.getCharsetValue("text/html; charset=utf-16"))
.isEqualTo(Optional.of("utf-16"));
}

@Test
void getCharsetValueEndOfStringWithSemiColon() {
Assertions.assertThat(HttpContentType.getCharsetValue("text/html; charset=utf-16;"))
.isEqualTo(Optional.of("utf-16"));
}

@Test
void getCharsetValue() {
Assertions.assertThat(
HttpContentType.getCharsetValue(
"multipart/form-data; charset=utf-16; boundary=ExampleBoundaryString"))
.isEqualTo(Optional.of("utf-16"));
}

@Test
void getCharsetValueWithSemiColon() {
Assertions.assertThat(
HttpContentType.getCharsetValue(
"multipart/form-data; charset=utf-16; boundary=ExampleBoundaryString;"))
.isEqualTo(Optional.of("utf-16"));
}
}

0 comments on commit 5a651b9

Please sign in to comment.