Skip to content

Commit cff1df0

Browse files
feat: Exceptions now supply ways to get the raw request and response (#614)
* feat: Exceptions from the SDK now have `getRawRequest()` and `getRawResponse()` for better debugging
1 parent 263fb2b commit cff1df0

11 files changed

Lines changed: 171 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
All notable changes to this project will be documented in this file.
33
This project adheres to [Semantic Versioning](http://semver.org/).
44

5+
# [9.10.0]
6+
- Exceptions: Added `getRawRequest()` and `getRawResponse()` methods to `VonageApiResponseException` for debugging API errors
7+
58
# [9.9.0]
69
- Video: Added post-call transcription options
710
- Video: Added connections list calls

README.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ Add the following to your `build.gradle` or `build.gradle.kts` file:
8383

8484
```groovy
8585
dependencies {
86-
implementation("com.vonage:server-sdk:9.9.0")
86+
implementation("com.vonage:server-sdk:9.10.0")
8787
}
8888
```
8989

@@ -94,7 +94,7 @@ Add the following to the `<dependencies>` section of your `pom.xml` file:
9494
<dependency>
9595
<groupId>com.vonage</groupId>
9696
<artifactId>server-sdk</artifactId>
97-
<version>9.9.0</version>
97+
<version>9.10.0</version>
9898
</dependency>
9999
```
100100

@@ -121,6 +121,30 @@ the dependency co-ordinates and add `mavenLocal()` to the `repositories` block i
121121
* For a searchable list of code snippets examples, see [**SNIPPETS.md**](https://github.com/Vonage/vonage-java-code-snippets/blob/main/SNIPPETS.md).
122122
* For Video API usage instructions, see [the guide on our developer portal](https://developer.vonage.com/en/video/server-sdks/java).
123123

124+
### Error Handling
125+
126+
When an API error occurs, the SDK will throw a `VonageApiResponseException` (or a subclass thereof). This exception
127+
contains structured information about the error, including the HTTP status code, error title, and detail message.
128+
129+
Starting with version 9.10.0, you can also access the raw HTTP request and response bodies for debugging purposes:
130+
131+
```java
132+
try {
133+
client.getMessagesClient().sendMessage(messageRequest);
134+
} catch (MessageResponseException ex) {
135+
System.err.println("Status: " + ex.getStatusCode());
136+
System.err.println("Title: " + ex.getTitle());
137+
System.err.println("Detail: " + ex.getDetail());
138+
139+
// Access raw request/response for debugging
140+
System.err.println("Raw Request: " + ex.getRawRequest());
141+
System.err.println("Raw Response: " + ex.getRawResponse());
142+
}
143+
```
144+
145+
This is particularly useful when troubleshooting API issues or working with Vonage support, as it allows you to see
146+
the exact JSON that was sent and received.
147+
124148
### Custom Requests
125149
Beginning with v9.1.0, you can now make customisable requests to any Vonage API endpoint using the `CustomClient` class,
126150
obtained from the `VonageClient#getCustomClient()` method. This will take care of auth and serialisation for you.

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
<groupId>com.vonage</groupId>
77
<artifactId>server-sdk</artifactId>
8-
<version>9.9.0</version>
8+
<version>9.10.0</version>
99

1010
<name>Vonage Java Server SDK</name>
1111
<description>Java client for Vonage APIs</description>

src/main/java/com/vonage/client/DynamicEndpoint.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public class DynamicEndpoint<T, R> extends AbstractMethod<T, R> {
5252
protected final Class<? extends VonageApiResponseException> responseExceptionType;
5353
protected final Class<R> responseType;
5454
protected T cachedRequestBody;
55+
protected String cachedRequestBodyString;
5556

5657
protected DynamicEndpoint(Builder<T, R> builder) {
5758
super(builder.wrapper);
@@ -232,7 +233,8 @@ protected final RequestBuilder makeRequest(T requestBody) {
232233
applyQueryParams(((QueryParamsRequest) requestBody).makeParams(), rqb);
233234
}
234235
if (requestBody instanceof Jsonable) {
235-
rqb.setEntity(new StringEntity(((Jsonable) requestBody).toJson(), ContentType.APPLICATION_JSON));
236+
cachedRequestBodyString = ((Jsonable) requestBody).toJson();
237+
rqb.setEntity(new StringEntity(cachedRequestBodyString, ContentType.APPLICATION_JSON));
236238
}
237239
else if (requestBody instanceof BinaryRequest) {
238240
BinaryRequest bin = (BinaryRequest) requestBody;
@@ -266,6 +268,7 @@ protected final R parseResponse(HttpResponse response) throws IOException {
266268
}
267269
finally {
268270
cachedRequestBody = null;
271+
cachedRequestBodyString = null;
269272
}
270273
}
271274

@@ -345,6 +348,10 @@ private R parseResponseFailure(HttpResponse response) throws IOException {
345348
varex.title = response.getStatusLine().getReasonPhrase();
346349
}
347350
varex.statusCode = response.getStatusLine().getStatusCode();
351+
varex.setRawResponse(exMessage);
352+
if (cachedRequestBodyString != null) {
353+
varex.setRawRequest(cachedRequestBodyString);
354+
}
348355
logger.log(Level.WARNING, "Failed to parse response", varex);
349356
throw varex;
350357
}

src/main/java/com/vonage/client/HttpWrapper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
public class HttpWrapper {
3838
private static final String
3939
CLIENT_NAME = "vonage-java-sdk",
40-
CLIENT_VERSION = "9.9.0",
40+
CLIENT_VERSION = "9.10.0",
4141
JAVA_VERSION = System.getProperty("java.version"),
4242
USER_AGENT = String.format("%s/%s java/%s", CLIENT_NAME, CLIENT_VERSION, JAVA_VERSION);
4343

src/main/java/com/vonage/client/VonageApiResponseException.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public class VonageApiResponseException extends VonageClientException implements
3434
protected String title, detail, instance, code, errorCodeLabel, errorCode;
3535
protected List<?> errors, invalidParameters;
3636
@JsonIgnore protected int statusCode;
37+
@JsonIgnore protected String rawResponse, rawRequest;
3738

3839
protected VonageApiResponseException() {
3940
}
@@ -162,6 +163,50 @@ public int getStatusCode() {
162163
return statusCode;
163164
}
164165

166+
/**
167+
* Gets the raw HTTP response body that was returned from the API.
168+
* This is useful for debugging when the structured error fields don't provide enough detail.
169+
*
170+
* @return The complete HTTP response body as a string, or {@code null} if not captured.
171+
* @since 9.9.0
172+
*/
173+
@JsonIgnore
174+
public String getRawResponse() {
175+
return rawResponse;
176+
}
177+
178+
/**
179+
* Sets the raw HTTP response body. Intended for internal use.
180+
*
181+
* @param rawResponse The complete HTTP response body as a string.
182+
*/
183+
@JsonIgnore
184+
protected void setRawResponse(String rawResponse) {
185+
this.rawResponse = rawResponse;
186+
}
187+
188+
/**
189+
* Gets the raw HTTP request body that was sent to the API.
190+
* This is useful for debugging to see exactly what was sent when an error occurred.
191+
*
192+
* @return The complete HTTP request body as a string, or {@code null} if not captured or not applicable.
193+
* @since 9.9.0
194+
*/
195+
@JsonIgnore
196+
public String getRawRequest() {
197+
return rawRequest;
198+
}
199+
200+
/**
201+
* Sets the raw HTTP request body. Intended for internal use.
202+
*
203+
* @param rawRequest The complete HTTP request body as a string.
204+
*/
205+
@JsonIgnore
206+
protected void setRawRequest(String rawRequest) {
207+
this.rawRequest = rawRequest;
208+
}
209+
165210
@JsonIgnore
166211
@Override
167212
public String getMessage() {

src/test/java/com/vonage/client/VonageApiResponseExceptionTest.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
import com.fasterxml.jackson.annotation.JsonProperty;
1919
import static org.junit.jupiter.api.Assertions.*;
2020
import org.junit.jupiter.api.*;
21+
import java.io.IOException;
22+
import java.io.InputStream;
2123
import java.net.URI;
24+
import java.nio.charset.StandardCharsets;
2225
import java.util.Collections;
2326
import java.util.List;
2427

@@ -244,4 +247,35 @@ public void testEquals() {
244247
ex2.instance = expected.instance;
245248
assertEquals(expected, ex2);
246249
}
250+
251+
private String loadJsonResource(String filename) throws IOException {
252+
try (InputStream is = getClass().getResourceAsStream(filename)) {
253+
if (is == null) {
254+
throw new IOException("Could not find resource: " + filename);
255+
}
256+
byte[] buffer = new byte[1024];
257+
StringBuilder sb = new StringBuilder();
258+
int bytesRead;
259+
while ((bytesRead = is.read(buffer)) != -1) {
260+
sb.append(new String(buffer, 0, bytesRead, StandardCharsets.UTF_8));
261+
}
262+
return sb.toString().trim();
263+
}
264+
}
265+
266+
@Test
267+
public void testRawRequestAndResponse() throws IOException {
268+
String rawRequest = loadJsonResource("raw-request-sms.json");
269+
String rawResponse = loadJsonResource("error-422-invalid-channel.json");
270+
271+
ConcreteVonageApiResponseException ex = new ConcreteVonageApiResponseException();
272+
assertNull(ex.getRawRequest());
273+
assertNull(ex.getRawResponse());
274+
275+
ex.setRawRequest(rawRequest);
276+
ex.setRawResponse(rawResponse);
277+
278+
assertEquals(rawRequest, ex.getRawRequest());
279+
assertEquals(rawResponse, ex.getRawResponse());
280+
}
247281
}

src/test/java/com/vonage/client/messages/MessagesClientTest.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
import com.vonage.client.messages.whatsapp.*;
3434
import static org.junit.jupiter.api.Assertions.*;
3535
import org.junit.jupiter.api.*;
36+
import java.io.IOException;
37+
import java.io.InputStream;
38+
import java.nio.charset.StandardCharsets;
3639
import java.util.*;
3740

3841
public class MessagesClientTest extends AbstractClientTest<MessagesClient> {
@@ -135,6 +138,53 @@ public void testSendMessage401Response() throws Exception {
135138
);
136139
}
137140

141+
private String loadJsonResource(String filename) throws IOException {
142+
try (InputStream is = getClass().getResourceAsStream(filename)) {
143+
if (is == null) {
144+
throw new IOException("Could not find resource: " + filename);
145+
}
146+
byte[] buffer = new byte[1024];
147+
StringBuilder sb = new StringBuilder();
148+
int bytesRead;
149+
while ((bytesRead = is.read(buffer)) != -1) {
150+
sb.append(new String(buffer, 0, bytesRead, StandardCharsets.UTF_8));
151+
}
152+
return sb.toString().trim();
153+
}
154+
}
155+
156+
@Test
157+
public void testSendMessageExceptionContainsRawRequestAndResponse() throws Exception {
158+
String responseJson = loadJsonResource("error-422-invalid-channel.json");
159+
stubResponse(422, responseJson);
160+
161+
MessageRequest request = SmsTextRequest.builder()
162+
.from("447700900001")
163+
.to("447700900000")
164+
.text("Hello").build();
165+
166+
try {
167+
client.useRegularEndpoint().sendMessage(request);
168+
fail("Expected MessageResponseException to be thrown");
169+
}
170+
catch (MessageResponseException ex) {
171+
// Verify the structured fields are set
172+
assertEquals(422, ex.getStatusCode());
173+
assertEquals("Invalid channel parameters", ex.getTitle());
174+
175+
// Verify the raw response is captured
176+
assertNotNull(ex.getRawResponse());
177+
assertTrue(ex.getRawResponse().contains("Invalid channel parameters"));
178+
assertTrue(ex.getRawResponse().contains("1110"));
179+
180+
// Verify the raw request is captured
181+
assertNotNull(ex.getRawRequest());
182+
assertTrue(ex.getRawRequest().contains("447700900000"));
183+
assertTrue(ex.getRawRequest().contains("447700900001"));
184+
assertTrue(ex.getRawRequest().contains("Hello"));
185+
}
186+
}
187+
138188
@Test
139189
public void testSensSmsSandboxFailure() {
140190
assertThrows(MessageResponseException.class, () -> client.useSandboxEndpoint()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"type":"https://developer.vonage.com/api-errors/messages#1110","title":"Invalid channel parameters","detail":"The value of one or more parameters is invalid.","instance":"bf0ca0bf927b3b52e3cb03217e1a1ddf"}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"type":"https://developer.vonage.com/api-errors/messages#1110","title":"Invalid channel parameters","detail":"The value of one or more parameters is invalid.","instance":"bf0ca0bf927b3b52e3cb03217e1a1ddf"}

0 commit comments

Comments
 (0)