Skip to content

Commit 48d08c8

Browse files
feat: port PR #273 changes — raw body support, URL encoding, null-safe request ID
- Add EMPTY_STRING, QUOTE, HTTPS_PROTOCOL, CURLY_PLACEHOLDER and HttpUtilityExtra (RAW_BODY_KEY, SDK_GENERATED_PREFIX) to Constants - HttpUtility: conditional content-type header, __raw_body__ passthrough, UUID fallback when server omits x-request-id, URL-encoded form params - Utils: URL-encode path and query params with graceful fallback - Validations: accept String request bodies for non-JSON content types - ConnectionController: wrap String bodies in __raw_body__ for non-JSON content types; fall back to raw string when response is not JSON - InfoLogs: "Bearer token is expired" → "Bearer token is invalid or expired" - HttpUtilityTests: add raw body, no content-type, null request ID and special-character form-encoding tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 6b6c25d commit 48d08c8

7 files changed

Lines changed: 162 additions & 20 deletions

File tree

src/main/java/com/skyflow/logs/InfoLogs.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public enum InfoLogs {
1414

1515
// Bearer token generation
1616
EMPTY_BEARER_TOKEN("Bearer token is empty."),
17-
BEARER_TOKEN_EXPIRED("Bearer token is expired."),
17+
BEARER_TOKEN_EXPIRED("Bearer token is invalid or expired."),
1818
GET_BEARER_TOKEN_TRIGGERED("getBearerToken method triggered."),
1919
GET_BEARER_TOKEN_SUCCESS("Bearer token generated."),
2020
GET_SIGNED_DATA_TOKENS_TRIGGERED("getSignedDataTokens method triggered."),

src/main/java/com/skyflow/utils/Constants.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ public final class Constants {
3434
public static final String PROCESSED_FILE_NAME_PREFIX = "processed-";
3535
public static final String ERROR_FROM_CLIENT_HEADER_KEY = "error-from-client";
3636
public static final String DEIDENTIFIED_FILE_PREFIX = "deidentified";
37+
public static final String HTTPS_PROTOCOL = "https";
38+
public static final String CURLY_PLACEHOLDER = "{%s}";
39+
public static final String EMPTY_STRING = "";
40+
public static final String QUOTE = "\"";
41+
42+
public static final class HttpUtilityExtra {
43+
public static final String RAW_BODY_KEY = "__raw_body__";
44+
public static final String SDK_GENERATED_PREFIX = "SDK-Generated-";
45+
private HttpUtilityExtra() {}
46+
}
3747

3848
static {
3949
String sdkVersion;

src/main/java/com/skyflow/utils/HttpUtility.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
import java.io.*;
88
import java.net.HttpURLConnection;
99
import java.net.URL;
10+
import java.net.URLEncoder;
1011
import java.nio.charset.StandardCharsets;
1112
import java.util.HashMap;
1213
import java.util.List;
1314
import java.util.Map;
1415
import java.util.Objects;
16+
import java.util.UUID;
1517

1618
public final class HttpUtility {
1719

@@ -32,8 +34,11 @@ public static String sendRequest(String method, URL url, JsonObject params, Map<
3234
try {
3335
connection = (HttpURLConnection) url.openConnection();
3436
connection.setRequestMethod(method);
35-
connection.setRequestProperty("content-type", "application/json");
3637
connection.setRequestProperty("Accept", "*/*");
38+
boolean hasContentType = headers != null && headers.containsKey("content-type");
39+
if (!hasContentType && params != null && !params.isEmpty()) {
40+
connection.setRequestProperty("content-type", "application/json");
41+
}
3742

3843
if (headers != null && !headers.isEmpty()) {
3944
for (Map.Entry<String, String> entry : headers.entrySet())
@@ -52,9 +57,11 @@ public static String sendRequest(String method, URL url, JsonObject params, Map<
5257
byte[] input = null;
5358
String requestContentType = connection.getRequestProperty("content-type");
5459

55-
if (requestContentType.contains("application/x-www-form-urlencoded")) {
60+
if (params.has(Constants.HttpUtilityExtra.RAW_BODY_KEY) && params.size() == 1) {
61+
input = params.get(Constants.HttpUtilityExtra.RAW_BODY_KEY).getAsString().getBytes(StandardCharsets.UTF_8);
62+
} else if (requestContentType != null && requestContentType.contains("application/x-www-form-urlencoded")) {
5663
input = formatJsonToFormEncodedString(params).getBytes(StandardCharsets.UTF_8);
57-
} else if (requestContentType.contains("multipart/form-data")) {
64+
} else if (requestContentType != null && requestContentType.contains("multipart/form-data")) {
5865
input = formatJsonToMultiPartFormDataString(params, boundary).getBytes(StandardCharsets.UTF_8);
5966
} else {
6067
input = params.toString().getBytes(StandardCharsets.UTF_8);
@@ -67,7 +74,11 @@ public static String sendRequest(String method, URL url, JsonObject params, Map<
6774

6875
int httpCode = connection.getResponseCode();
6976
String requestID = connection.getHeaderField("x-request-id");
70-
HttpUtility.requestID = requestID.split(",")[0];
77+
if (requestID != null) {
78+
HttpUtility.requestID = requestID.split(",")[0];
79+
} else {
80+
HttpUtility.requestID = Constants.HttpUtilityExtra.SDK_GENERATED_PREFIX + UUID.randomUUID();
81+
}
7182
Map<String, List<String>> responseHeaders = connection.getHeaderFields();
7283
Reader streamReader;
7384
if (httpCode > 299) {
@@ -159,7 +170,13 @@ public static String appendRequestId(String message, String requestId) {
159170
}
160171

161172
private static String makeFormEncodeKeyValuePair(String key, String value) {
162-
return key + "=" + value + "&";
173+
try {
174+
String encodedKey = URLEncoder.encode(key, StandardCharsets.UTF_8.toString());
175+
String encodedValue = URLEncoder.encode(value, StandardCharsets.UTF_8.toString());
176+
return encodedKey + "=" + encodedValue + "&";
177+
} catch (Exception e) {
178+
return key + "=" + value + "&";
179+
}
163180
}
164181

165182
}

src/main/java/com/skyflow/utils/Utils.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import java.io.File;
1818
import java.net.MalformedURLException;
1919
import java.net.URL;
20+
import java.net.URLEncoder;
21+
import java.nio.charset.StandardCharsets;
2022
import java.security.KeyFactory;
2123
import java.security.NoSuchAlgorithmException;
2224
import java.security.PrivateKey;
@@ -119,7 +121,12 @@ public static String constructConnectionURL(ConnectionConfig config, InvokeConne
119121
for (Map.Entry<String, String> entry : invokeConnectionRequest.getPathParams().entrySet()) {
120122
String key = entry.getKey();
121123
String value = entry.getValue();
122-
filledURL = new StringBuilder(filledURL.toString().replace(String.format("{%s}", key), value));
124+
try {
125+
String encodedValue = URLEncoder.encode(value, StandardCharsets.UTF_8.name());
126+
filledURL = new StringBuilder(filledURL.toString().replace(String.format(Constants.CURLY_PLACEHOLDER, key), encodedValue));
127+
} catch (Exception e) {
128+
filledURL = new StringBuilder(filledURL.toString().replace(String.format(Constants.CURLY_PLACEHOLDER, key), value));
129+
}
123130
}
124131
}
125132

@@ -128,7 +135,13 @@ public static String constructConnectionURL(ConnectionConfig config, InvokeConne
128135
for (Map.Entry<String, String> entry : invokeConnectionRequest.getQueryParams().entrySet()) {
129136
String key = entry.getKey();
130137
String value = entry.getValue();
131-
filledURL.append(key).append("=").append(value).append("&");
138+
try {
139+
String encodedKey = URLEncoder.encode(key, StandardCharsets.UTF_8.name());
140+
String encodedValue = URLEncoder.encode(value, StandardCharsets.UTF_8.name());
141+
filledURL.append(encodedKey).append("=").append(encodedValue).append("&");
142+
} catch (Exception e) {
143+
filledURL.append(key).append("=").append(value).append("&");
144+
}
132145
}
133146
filledURL = new StringBuilder(filledURL.substring(0, filledURL.length() - 1));
134147
}

src/main/java/com/skyflow/utils/validations/Validations.java

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import java.util.regex.Pattern;
1111

1212
import com.google.gson.Gson;
13+
import com.google.gson.JsonElement;
1314
import com.google.gson.JsonObject;
1415
import com.skyflow.config.ConnectionConfig;
1516
import com.skyflow.config.Credentials;
@@ -146,12 +147,27 @@ public static void validateInvokeConnectionRequest(InvokeConnectionRequest invok
146147
}
147148

148149
if (requestBody != null) {
149-
Gson gson = new Gson();
150-
JsonObject bodyObject = gson.toJsonTree(requestBody).getAsJsonObject();
151-
if (bodyObject.isEmpty()) {
152-
LogUtil.printErrorLog(Utils.parameterizedString(
153-
ErrorLogs.EMPTY_REQUEST_BODY.getLog(), InterfaceName.INVOKE_CONNECTION.getName()));
154-
throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyRequestBody.getMessage());
150+
if (requestBody.getClass().equals(Object.class)) {
151+
return;
152+
}
153+
if (requestBody instanceof String) {
154+
String bodyStr = (String) requestBody;
155+
if (bodyStr.trim().isEmpty()) {
156+
LogUtil.printErrorLog(Utils.parameterizedString(
157+
ErrorLogs.EMPTY_REQUEST_BODY.getLog(), InterfaceName.INVOKE_CONNECTION.getName()));
158+
throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyRequestBody.getMessage());
159+
}
160+
} else {
161+
Gson gson = new Gson();
162+
JsonElement bodyElement = gson.toJsonTree(requestBody);
163+
if (bodyElement.isJsonObject()) {
164+
JsonObject bodyObject = bodyElement.getAsJsonObject();
165+
if (bodyObject.isEmpty()) {
166+
LogUtil.printErrorLog(Utils.parameterizedString(
167+
ErrorLogs.EMPTY_REQUEST_BODY.getLog(), InterfaceName.INVOKE_CONNECTION.getName()));
168+
throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyRequestBody.getMessage());
169+
}
170+
}
155171
}
156172
}
157173
}

src/main/java/com/skyflow/vault/controller/ConnectionController.java

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,37 @@ public InvokeConnectionResponse invoke(InvokeConnectionRequest invokeConnectionR
5555
Object requestBodyObject = invokeConnectionRequest.getRequestBody();
5656

5757
if (requestBodyObject != null) {
58-
try {
59-
requestBody = convertObjectToJson(requestBodyObject);
60-
} catch (Exception e) {
61-
LogUtil.printErrorLog(ErrorLogs.INVALID_REQUEST_HEADERS.getLog());
62-
throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidRequestBody.getMessage());
58+
if (requestBodyObject instanceof String) {
59+
String contentType = headers.getOrDefault("content-type", "");
60+
if (!contentType.isEmpty() && !contentType.toLowerCase().contains("application/json")) {
61+
requestBody = new JsonObject();
62+
requestBody.addProperty(Constants.HttpUtilityExtra.RAW_BODY_KEY, (String) requestBodyObject);
63+
} else {
64+
try {
65+
requestBody = convertObjectToJson(requestBodyObject);
66+
} catch (Exception e) {
67+
LogUtil.printErrorLog(ErrorLogs.INVALID_REQUEST_HEADERS.getLog());
68+
throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidRequestBody.getMessage());
69+
}
70+
}
71+
} else {
72+
try {
73+
requestBody = convertObjectToJson(requestBodyObject);
74+
} catch (Exception e) {
75+
LogUtil.printErrorLog(ErrorLogs.INVALID_REQUEST_HEADERS.getLog());
76+
throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidRequestBody.getMessage());
77+
}
6378
}
6479
}
6580

6681
String response = HttpUtility.sendRequest(requestMethod.name(), new URL(filledURL), requestBody, headers);
67-
JsonObject data = JsonParser.parseString(response).getAsJsonObject();
82+
JsonObject data;
83+
try {
84+
data = JsonParser.parseString(response).getAsJsonObject();
85+
} catch (Exception e) {
86+
data = new JsonObject();
87+
data.addProperty("response", response);
88+
}
6889
HashMap<String, String> metadata = new HashMap<>();
6990
metadata.put("requestId", HttpUtility.getRequestID());
7091
connectionResponse = new InvokeConnectionResponse(data, metadata, null);

src/test/java/com/skyflow/utils/HttpUtilityTests.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,69 @@ public void testSendRequestError() {
124124
fail(INVALID_EXCEPTION_THROWN);
125125
}
126126
}
127+
128+
@Test
129+
@PrepareForTest({URL.class, HttpURLConnection.class})
130+
public void testSendRequestWithRawBody() {
131+
try {
132+
given(mockConnection.getRequestProperty("content-type")).willReturn("application/xml");
133+
Map<String, String> headers = new HashMap<>();
134+
headers.put("content-type", "application/xml");
135+
JsonObject params = new JsonObject();
136+
params.addProperty("__raw_body__", "<xml>test</xml>");
137+
String response = httpUtility.sendRequest("POST", url, params, headers);
138+
Assert.assertEquals(expected, response);
139+
} catch (Exception e) {
140+
fail(INVALID_EXCEPTION_THROWN);
141+
}
142+
}
143+
144+
@Test
145+
@PrepareForTest({URL.class, HttpURLConnection.class})
146+
public void testSendRequestWithoutContentTypeHeader() {
147+
try {
148+
given(mockConnection.getRequestProperty("content-type")).willReturn("application/json");
149+
JsonObject params = new JsonObject();
150+
params.addProperty("key", "value");
151+
String response = httpUtility.sendRequest("POST", url, params, null);
152+
Assert.assertEquals(expected, response);
153+
} catch (Exception e) {
154+
fail(INVALID_EXCEPTION_THROWN);
155+
}
156+
}
157+
158+
@Test
159+
@PrepareForTest({URL.class, HttpURLConnection.class})
160+
public void testSendRequestWithNullRequestId() {
161+
try {
162+
given(mockConnection.getHeaderField(anyString())).willReturn(null);
163+
given(mockConnection.getRequestProperty("content-type")).willReturn("application/json");
164+
Map<String, String> headers = new HashMap<>();
165+
headers.put("content-type", "application/json");
166+
JsonObject params = new JsonObject();
167+
params.addProperty("key", "value");
168+
String response = httpUtility.sendRequest("GET", url, params, headers);
169+
Assert.assertEquals(expected, response);
170+
Assert.assertNotNull(HttpUtility.getRequestID());
171+
} catch (Exception e) {
172+
fail(INVALID_EXCEPTION_THROWN);
173+
}
174+
}
175+
176+
@Test
177+
@PrepareForTest({URL.class, HttpURLConnection.class})
178+
public void testSendRequestFormURLEncodedWithSpecialCharacters() {
179+
try {
180+
given(mockConnection.getRequestProperty("content-type")).willReturn("application/x-www-form-urlencoded");
181+
Map<String, String> headers = new HashMap<>();
182+
headers.put("content-type", "application/x-www-form-urlencoded");
183+
JsonObject params = new JsonObject();
184+
params.addProperty("key", "value with spaces");
185+
params.addProperty("special", "test@email.com");
186+
String response = httpUtility.sendRequest("POST", url, params, headers);
187+
Assert.assertEquals(expected, response);
188+
} catch (Exception e) {
189+
fail(INVALID_EXCEPTION_THROWN);
190+
}
191+
}
127192
}

0 commit comments

Comments
 (0)