Skip to content

Commit c2773e3

Browse files
fix: Inconsistent exception handling and lack of structured status code (#59)
1 parent e3e9825 commit c2773e3

File tree

9 files changed

+179
-53
lines changed

9 files changed

+179
-53
lines changed

build.gradle

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ dependencies {
2828
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.2'
2929

3030
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
31+
implementation("org.json:json:20250517")
3132
}
3233

3334
test {
@@ -86,8 +87,8 @@ publishing {
8687
}
8788
repositories {
8889
maven {
89-
name = 'nexus'
90-
url = uri("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/")
90+
name = 'ossrh-staging-api'
91+
url = uri("https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/")
9192
credentials {
9293
username = USERNAME
9394
password = PASSWORD

src/main/java/com/resend/core/exception/ResendException.java

Lines changed: 132 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,30 @@
11
package com.resend.core.exception;
22

3+
import org.json.JSONException;
4+
import org.json.JSONObject;
5+
36
/**
4-
* Custom exception class for representing errors related to the Resend API.
7+
* Exception class for representing errors related to the Resend API.
58
*/
69
public class ResendException extends Exception {
710

11+
private static final long serialVersionUID = 1L;
12+
13+
/**
14+
* The HTTP status code of the response that triggered this exception.
15+
*/
16+
private Integer statusCode;
17+
18+
/**
19+
* The error name/type returned.
20+
*/
21+
private String errorName;
22+
23+
/**
24+
* The raw response body from the API.
25+
*/
26+
private String responseBody;
27+
828
/**
929
* Constructs a new `ResendException` with the specified error message.
1030
*
@@ -23,5 +43,115 @@ public ResendException(String message) {
2343
public ResendException(String message, Throwable cause) {
2444
super(message, cause);
2545
}
26-
}
2746

47+
/**
48+
* Constructs a new `ResendException` with HTTP status code and response body.
49+
* Automatically parses the JSON response to extract error details if possible.
50+
*
51+
* @param statusCode The HTTP status code.
52+
* @param responseBody The raw response body from the API.
53+
*/
54+
public ResendException(int statusCode, String responseBody) {
55+
super(parseMessage(responseBody));
56+
this.statusCode = statusCode;
57+
this.responseBody = responseBody;
58+
this.errorName = parseErrorName(responseBody);
59+
}
60+
61+
/**
62+
* Constructs a new `ResendException` with a custom message, status code, and response body.
63+
*
64+
* @param message The error message describing the exception.
65+
* @param statusCode The HTTP status code.
66+
* @param responseBody The raw response body from the API.
67+
*/
68+
public ResendException(String message, int statusCode, String responseBody) {
69+
super(message);
70+
this.statusCode = statusCode;
71+
this.responseBody = responseBody;
72+
this.errorName = parseErrorName(responseBody);
73+
}
74+
75+
/**
76+
* Parses the error message from the JSON response body.
77+
* Falls back to the raw response body if parsing fails.
78+
*
79+
* @param responseBody The raw response body.
80+
* @return The parsed error message or the raw response body.
81+
*/
82+
private static String parseMessage(String responseBody) {
83+
if (responseBody == null || responseBody.isEmpty()) {
84+
return "Unknown error";
85+
}
86+
87+
try {
88+
JSONObject json = new JSONObject(responseBody);
89+
if (json.has("message")) {
90+
return json.getString("message");
91+
}
92+
} catch (JSONException e) {
93+
// Not valid JSON, return raw body
94+
}
95+
96+
return responseBody;
97+
}
98+
99+
/**
100+
* Parses the error name from the JSON response body.
101+
*
102+
* @param responseBody The raw response body.
103+
* @return The error name if found, null otherwise.
104+
*/
105+
private static String parseErrorName(String responseBody) {
106+
if (responseBody == null || responseBody.isEmpty()) {
107+
return null;
108+
}
109+
110+
try {
111+
JSONObject json = new JSONObject(responseBody);
112+
if (json.has("name")) {
113+
return json.getString("name");
114+
}
115+
} catch (JSONException e) {
116+
// Not valid JSON or no name field
117+
}
118+
119+
return null;
120+
}
121+
122+
/**
123+
* Returns the HTTP status code of the response that triggered this exception.
124+
*
125+
* @return the HTTP status code, or null if not set.
126+
*/
127+
public Integer getStatusCode() {
128+
return statusCode;
129+
}
130+
131+
/**
132+
* Returns the error name/type.
133+
*
134+
* @return the error name (e.g., "validation_error"), or null if not set.
135+
*/
136+
public String getErrorName() {
137+
return errorName;
138+
}
139+
140+
/**
141+
* Returns the raw response body from the API.
142+
*
143+
* @return the response body, or null if not set.
144+
*/
145+
public String getResponseBody() {
146+
return responseBody;
147+
}
148+
149+
@Override
150+
public String toString() {
151+
return "ResendException{" +
152+
"statusCode=" + statusCode +
153+
", errorName='" + errorName + '\'' +
154+
", message='" + getMessage() + '\'' +
155+
'}';
156+
}
157+
}

src/main/java/com/resend/services/apikeys/ApiKeys.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public CreateApiKeyResponse create(CreateApiKeyOptions createApiKeyOptions) thro
3737
AbstractHttpResponse<String> response = httpClient.perform("/api-keys", super.apiKey, HttpMethod.POST, payload, MediaType.get("application/json"));
3838

3939
if (!response.isSuccessful()) {
40-
throw new ResendException("Failed to create api key: " + response.getCode() + " " + response.getBody());
40+
throw new ResendException(response.getCode(), response.getBody());
4141
}
4242

4343
String responseBody = response.getBody();
@@ -56,7 +56,7 @@ public ListApiKeysResponse list() throws ResendException {
5656
AbstractHttpResponse<String> response = this.httpClient.perform("/api-keys", super.apiKey, HttpMethod.GET, null, MediaType.get("application/json"));
5757

5858
if (!response.isSuccessful()) {
59-
throw new ResendException("Failed to retrieve api keys: " + response.getCode() + " " + response.getBody());
59+
throw new ResendException(response.getCode(), response.getBody());
6060
}
6161

6262
String responseBody = response.getBody();
@@ -77,7 +77,7 @@ public ListApiKeysResponse list(ListParams params) throws ResendException {
7777
AbstractHttpResponse<String> response = this.httpClient.perform(pathWithQuery, super.apiKey, HttpMethod.GET, null, MediaType.get("application/json"));
7878

7979
if (!response.isSuccessful()) {
80-
throw new ResendException("Failed to retrieve api keys: " + response.getCode() + " " + response.getBody());
80+
throw new ResendException(response.getCode(), response.getBody());
8181
}
8282

8383
String responseBody = response.getBody();
@@ -97,7 +97,7 @@ public boolean remove(String apiKeyId) throws ResendException {
9797
AbstractHttpResponse<String> response = httpClient.perform("/api-keys/" + apiKeyId, super.apiKey, HttpMethod.DELETE, "", null);
9898

9999
if (!response.isSuccessful()) {
100-
throw new ResendException("Failed to delete api key: " + response.getCode() + " " + response.getBody());
100+
throw new ResendException(response.getCode(), response.getBody());
101101
}
102102

103103
return true;

src/main/java/com/resend/services/audiences/Audiences.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public CreateAudienceResponseSuccess create(CreateAudienceOptions createAudience
3535
AbstractHttpResponse<String> response = httpClient.perform("/audiences", super.apiKey, HttpMethod.POST, payload, MediaType.get("application/json"));
3636

3737
if (!response.isSuccessful()) {
38-
throw new ResendException("Failed to create Audience: " + response.getCode() + " " + response.getBody());
38+
throw new ResendException(response.getCode(), response.getBody());
3939
}
4040

4141
String responseBody = response.getBody();
@@ -52,7 +52,7 @@ public ListAudiencesResponseSuccess list() throws ResendException {
5252
AbstractHttpResponse<String> response = this.httpClient.perform("/audiences", super.apiKey, HttpMethod.GET, null, MediaType.get("application/json"));
5353

5454
if (!response.isSuccessful()) {
55-
throw new ResendException("Failed to retrieve audiences: " + response.getCode() + " " + response.getBody());
55+
throw new ResendException(response.getCode(), response.getBody());
5656
}
5757

5858
String responseBody = response.getBody();
@@ -72,7 +72,7 @@ public ListAudiencesResponseSuccess list(ListParams params) throws ResendExcepti
7272
AbstractHttpResponse<String> response = this.httpClient.perform(pathWithQuery, super.apiKey, HttpMethod.GET, null, MediaType.get("application/json"));
7373

7474
if (!response.isSuccessful()) {
75-
throw new ResendException("Failed to retrieve audiences: " + response.getCode() + " " + response.getBody());
75+
throw new ResendException(response.getCode(), response.getBody());
7676
}
7777

7878
String responseBody = response.getBody();
@@ -91,7 +91,7 @@ public GetAudienceResponseSuccess get(String id) throws ResendException {
9191
AbstractHttpResponse<String> response = this.httpClient.perform("/audiences/" +id, super.apiKey, HttpMethod.GET, null, MediaType.get("application/json"));
9292

9393
if (!response.isSuccessful()) {
94-
throw new RuntimeException("Failed to retrieve audience: " + response.getCode() + " " + response.getBody());
94+
throw new ResendException(response.getCode(), response.getBody());
9595
}
9696

9797
String responseBody = response.getBody();
@@ -110,7 +110,7 @@ public RemoveAudienceResponseSuccess remove(String id) throws ResendException {
110110
AbstractHttpResponse<String> response = httpClient.perform("/audiences/" +id, super.apiKey, HttpMethod.DELETE, "", null);
111111

112112
if (!response.isSuccessful()) {
113-
throw new ResendException("Failed to delete audience: " + response.getCode() + " " + response.getBody());
113+
throw new ResendException(response.getCode(), response.getBody());
114114
}
115115

116116
String responseBody = response.getBody();

src/main/java/com/resend/services/batch/Batch.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public CreateBatchEmailsResponse send(List<CreateEmailOptions> emails) throws Re
3838
AbstractHttpResponse<String> response = super.httpClient.perform("/emails/batch", super.apiKey, HttpMethod.POST, payload, MediaType.get("application/json"));
3939

4040
if (!response.isSuccessful()) {
41-
throw new RuntimeException("Failed to send batch emails: " + response.getCode() + " " + response.getBody());
41+
throw new ResendException(response.getCode(), response.getBody());
4242
}
4343

4444
String responseBody = response.getBody();
@@ -62,7 +62,7 @@ public CreateBatchEmailsResponse send(List<CreateEmailOptions> emails, RequestOp
6262
AbstractHttpResponse<String> response = super.httpClient.perform("/emails/batch", super.apiKey, HttpMethod.POST, payload, MediaType.get("application/json"), requestOptions);
6363

6464
if (!response.isSuccessful()) {
65-
throw new RuntimeException("Failed to send batch emails: " + response.getCode() + " " + response.getBody());
65+
throw new ResendException(response.getCode(), response.getBody());
6666
}
6767

6868
String responseBody = response.getBody();

src/main/java/com/resend/services/broadcasts/Broadcasts.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public CreateBroadcastResponseSuccess create(CreateBroadcastOptions createBroadc
3636
AbstractHttpResponse<String> response = httpClient.perform("/broadcasts", super.apiKey, HttpMethod.POST, payload, MediaType.get("application/json"));
3737

3838
if (!response.isSuccessful()) {
39-
throw new ResendException("Failed to create Broadcast: " + response.getCode() + " " + response.getBody());
39+
throw new ResendException(response.getCode(), response.getBody());
4040
}
4141

4242
String responseBody = response.getBody();
@@ -54,7 +54,7 @@ public GetBroadcastResponseSuccess get(String id) throws ResendException {
5454
AbstractHttpResponse<String> response = this.httpClient.perform("/broadcasts/" +id, super.apiKey, HttpMethod.GET, null, MediaType.get("application/json"));
5555

5656
if (!response.isSuccessful()) {
57-
throw new RuntimeException("Failed to retrieve broadcast: " + response.getCode() + " " + response.getBody());
57+
throw new ResendException(response.getCode(), response.getBody());
5858
}
5959

6060
String responseBody = response.getBody();
@@ -75,7 +75,7 @@ public SendBroadcastResponseSuccess send(SendBroadcastOptions sendBroadcastOptio
7575
AbstractHttpResponse<String> response = httpClient.perform("/broadcasts/" +broadcastId + "/send", super.apiKey, HttpMethod.POST, payload, MediaType.get("application/json"));
7676

7777
if (!response.isSuccessful()) {
78-
throw new ResendException("Failed to send broadcast: " + response.getCode() + " " + response.getBody());
78+
throw new ResendException(response.getCode(), response.getBody());
7979
}
8080

8181
String responseBody = response.getBody();
@@ -93,7 +93,7 @@ public RemoveBroadcastResponseSuccess remove(String id) throws ResendException {
9393
AbstractHttpResponse<String> response = httpClient.perform("/broadcasts/" +id, super.apiKey, HttpMethod.DELETE, "", null);
9494

9595
if (!response.isSuccessful()) {
96-
throw new ResendException("Failed to delete broadcast: " + response.getCode() + " " + response.getBody());
96+
throw new ResendException(response.getCode(), response.getBody());
9797
}
9898

9999
String responseBody = response.getBody();
@@ -111,7 +111,7 @@ public ListBroadcastsResponseSuccess list() throws ResendException {
111111
AbstractHttpResponse<String> response = this.httpClient.perform("/broadcasts", super.apiKey, HttpMethod.GET, null, MediaType.get("application/json"));
112112

113113
if (!response.isSuccessful()) {
114-
throw new ResendException("Failed to retrieve broadcasts: " + response.getCode() + " " + response.getBody());
114+
throw new ResendException(response.getCode(), response.getBody());
115115
}
116116

117117
String responseBody = response.getBody();
@@ -131,7 +131,7 @@ public ListBroadcastsResponseSuccess list(ListParams params) throws ResendExcept
131131
AbstractHttpResponse<String> response = this.httpClient.perform(pathWithQuery, super.apiKey, HttpMethod.GET, null, MediaType.get("application/json"));
132132

133133
if (!response.isSuccessful()) {
134-
throw new ResendException("Failed to retrieve broadcasts: " + response.getCode() + " " + response.getBody());
134+
throw new ResendException(response.getCode(), response.getBody());
135135
}
136136

137137
String responseBody = response.getBody();
@@ -151,7 +151,7 @@ public UpdateBroadcastResponseSuccess update(UpdateBroadcastOptions updateBroadc
151151
AbstractHttpResponse<String> response = httpClient.perform("/broadcasts/"+updateBroadcastOptions.getId(), super.apiKey, HttpMethod.PATCH, payload, MediaType.get("application/json"));
152152

153153
if (!response.isSuccessful()) {
154-
throw new ResendException("Failed to update Broadcast: " + response.getCode() + " " + response.getBody());
154+
throw new ResendException(response.getCode(), response.getBody());
155155
}
156156

157157
String responseBody = response.getBody();

src/main/java/com/resend/services/contacts/Contacts.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public CreateContactResponseSuccess create(CreateContactOptions createContactOpt
3535
AbstractHttpResponse<String> response = httpClient.perform("/audiences/" + createContactOptions.getAudienceId()+ "/contacts" , super.apiKey, HttpMethod.POST, payload, MediaType.get("application/json"));
3636

3737
if (!response.isSuccessful()) {
38-
throw new ResendException("Failed to create contact: " + response.getCode() + " " + response.getBody());
38+
throw new ResendException(response.getCode(), response.getBody());
3939
}
4040

4141
String responseBody = response.getBody();
@@ -53,7 +53,7 @@ public ListContactsResponseSuccess list(String audienceId) throws ResendExceptio
5353
AbstractHttpResponse<String> response = this.httpClient.perform("/audiences/" +audienceId+ "/contacts" , super.apiKey, HttpMethod.GET, null, MediaType.get("application/json"));
5454

5555
if (!response.isSuccessful()) {
56-
throw new ResendException("Failed to retrieve contacts: " + response.getCode() + " " + response.getBody());
56+
throw new ResendException(response.getCode(), response.getBody());
5757
}
5858

5959
String responseBody = response.getBody();
@@ -74,7 +74,7 @@ public ListContactsResponseSuccess list(String audienceId, ListParams params) th
7474
AbstractHttpResponse<String> response = this.httpClient.perform(pathWithQuery, super.apiKey, HttpMethod.GET, null, MediaType.get("application/json"));
7575

7676
if (!response.isSuccessful()) {
77-
throw new ResendException("Failed to retrieve contacts: " + response.getCode() + " " + response.getBody());
77+
throw new ResendException(response.getCode(), response.getBody());
7878
}
7979

8080
String responseBody = response.getBody();
@@ -105,7 +105,7 @@ public GetContactResponseSuccess get(GetContactOptions params) throws ResendExce
105105
AbstractHttpResponse<String> response = this.httpClient.perform("/audiences/" +params.getAudienceId()+ "/contacts/" +contactIdentifier, super.apiKey, HttpMethod.GET, null, MediaType.get("application/json"));
106106

107107
if (!response.isSuccessful()) {
108-
throw new RuntimeException("Failed to retrieve contact: " + response.getCode() + " " + response.getBody());
108+
throw new ResendException(response.getCode(), response.getBody());
109109
}
110110

111111
String responseBody = response.getBody();
@@ -131,7 +131,7 @@ public RemoveContactResponseSuccess remove(RemoveContactOptions params) throws R
131131
AbstractHttpResponse<String> response = httpClient.perform("/audiences/" +params.getAudienceId()+ "/contacts/" + pathParameter, super.apiKey, HttpMethod.DELETE, "", null);
132132

133133
if (!response.isSuccessful()) {
134-
throw new ResendException("Failed to delete contact: " + response.getCode() + " " + response.getBody());
134+
throw new ResendException(response.getCode(), response.getBody());
135135
}
136136

137137
String responseBody = response.getBody();
@@ -158,7 +158,7 @@ public UpdateContactResponseSuccess update(UpdateContactOptions params) throws R
158158
AbstractHttpResponse<String> response = httpClient.perform("/audiences/" +params.getAudienceId()+ "/contacts/" + pathParameter, super.apiKey, HttpMethod.PATCH, payload, MediaType.get("application/json"));
159159

160160
if (!response.isSuccessful()) {
161-
throw new ResendException("Failed to patch contact: " + response.getCode() + " " + response.getBody());
161+
throw new ResendException(response.getCode(), response.getBody());
162162
}
163163

164164
String responseBody = response.getBody();

0 commit comments

Comments
 (0)