From 686cb730a3a921d7cc0aa7c536f762d375a9f49d Mon Sep 17 00:00:00 2001 From: sbansla Date: Wed, 4 Feb 2026 12:14:03 +0530 Subject: [PATCH 1/2] chore: added exception as per rfc-9457 standards --- .../exception/RestStandardException.java | 198 ++++++++++++++++++ .../exception/RestStandardExceptionTest.java | 80 +++++++ 2 files changed, 278 insertions(+) create mode 100644 src/main/java/com/twilio/exception/RestStandardException.java create mode 100644 src/test/java/com/twilio/exception/RestStandardExceptionTest.java diff --git a/src/main/java/com/twilio/exception/RestStandardException.java b/src/main/java/com/twilio/exception/RestStandardException.java new file mode 100644 index 0000000000..595c63a3aa --- /dev/null +++ b/src/main/java/com/twilio/exception/RestStandardException.java @@ -0,0 +1,198 @@ +package com.twilio.exception; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.List; + +/** + * RFC-9457 Problem Details Exception. + * + * Represents error responses compliant with RFC-9457 (Problem Details for HTTP APIs). + * See https://www.rfc-editor.org/rfc/rfc9457.html for the specification. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class RestStandardException { + + private final String type; + private final String title; + private final Integer status; + private final String detail; + private final String instance; + private final Integer code; + private final List errors; + + /** + * Represents a validation error for a specific field. + */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static class ValidationError { + private final String detail; + private final String pointer; + + @JsonCreator + public ValidationError( + @JsonProperty("detail") final String detail, + @JsonProperty("pointer") final String pointer) { + this.detail = detail; + this.pointer = pointer; + } + + /** + * Get the human-readable explanation of the validation error. + * + * @return validation error detail + */ + public String getDetail() { + return detail; + } + + /** + * Get the JSON Pointer (RFC 6901) to the location in the request where the error occurred. + * + * @return JSON pointer to the error location + */ + public String getPointer() { + return pointer; + } + } + + /** + * Initialize a RFC-9457 Problem Details Exception with required fields + * + * @param type URI reference identifying the problem type + * @param title short, human-readable summary of the problem type + * @param status HTTP status code + * @param code Twilio-specific error code + */ + private RestStandardException( + @JsonProperty("code") final Integer code, + @JsonProperty("status") final Integer status, + @JsonProperty("type") final String type, + @JsonProperty("title") final String title) { + this.type = type; + this.title = title; + this.status = status; + this.code = code; + this.detail = ""; + this.instance = ""; + this.errors = Collections.emptyList(); + } + + /** + * Initialize a RFC-9457 Problem Details Exception. + * + * @param type URI reference identifying the problem type + * @param title short, human-readable summary of the problem type + * @param status HTTP status code + * @param detail human-readable explanation specific to this occurrence + * @param instance URI reference identifying the specific occurrence + * @param code Twilio-specific error code + * @param errors array of validation errors for HTTP 400/422 responses + */ + @JsonCreator + private RestStandardException( + @JsonProperty("code") final Integer code, + @JsonProperty("status") final Integer status, + @JsonProperty("type") final String type, + @JsonProperty("title") final String title, + @JsonProperty("detail") final String detail, + @JsonProperty("instance") final String instance, + @JsonProperty("errors") final List errors) { + super(); + this.type = type; + this.title = title; + this.status = status; + this.code = code; + this.detail = detail == null ? "" : detail; + this.instance = instance == null ? "" : instance; + this.errors = errors == null ? Collections.emptyList(): errors; + } + + /** + * Build an exception from a JSON blob. + * + * @param json JSON blob + * @param objectMapper JSON reader + * @return Problem Exception as an object + */ + public static RestStandardException fromJson(final InputStream json, final ObjectMapper objectMapper) { + try { + return objectMapper.readValue(json, RestStandardException.class); + } catch (final JsonMappingException | JsonParseException e) { + throw new ApiException(e.getMessage(), e); + } catch (final IOException e) { + throw new ApiConnectionException(e.getMessage(), e); + } + } + + /** + * Get the URI reference identifying the problem type. + * + * @return problem type URI + */ + public String getType() { + return type; + } + + /** + * Get the short, human-readable summary of the problem type. + * + * @return problem title + */ + public String getTitle() { + return title; + } + + /** + * Get the HTTP status code. + * + * @return HTTP status code + */ + public Integer getStatus() { + return status; + } + + /** + * Get the human-readable explanation specific to this occurrence. + * + * @return problem detail + */ + public String getDetail() { + return detail; + } + + /** + * Get the URI reference identifying the specific occurrence. + * + * @return instance URI + */ + public String getInstance() { + return instance; + } + + /** + * Get the Twilio-specific error code. + * + * @return Twilio error code + */ + public Integer getCode() { + return code; + } + + /** + * Get the array of validation errors for HTTP 400/422 responses. + * + * @return list of validation errors, or null if not present + */ + public List getErrors() { + return errors; + } +} diff --git a/src/test/java/com/twilio/exception/RestStandardExceptionTest.java b/src/test/java/com/twilio/exception/RestStandardExceptionTest.java new file mode 100644 index 0000000000..da170f4ce2 --- /dev/null +++ b/src/test/java/com/twilio/exception/RestStandardExceptionTest.java @@ -0,0 +1,80 @@ +package com.twilio.exception; + +import java.io.ByteArrayInputStream; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class RestStandardExceptionTest { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @Test + public void fromJsonWithAllFields() { + final String errorJson = "{\n" + + " \"type\": \"https://www.twilio.com/docs/api/errors/20001\",\n" + + " \"title\": \"Validation error\",\n" + + " \"status\": 400,\n" + + " \"detail\": \"The request contains invalid parameter values.\",\n" + + " \"instance\": \"/v1/Accounts/AC123/Calls\",\n" + + " \"code\": 20001,\n" + + " \"errors\": [\n" + + " {\n" + + " \"detail\": \"The 'From' parameter is required but was not provided.\",\n" + + " \"pointer\": \"#/From\"\n" + + " },\n" + + " {\n" + + " \"detail\": \"must be a positive integer\",\n" + + " \"pointer\": \"#/age\"\n" + + " }\n" + + " ]\n" + + "}"; + + final RestStandardException restStandardException = RestStandardException.fromJson( + new ByteArrayInputStream(errorJson.getBytes()), OBJECT_MAPPER); + + assertEquals("https://www.twilio.com/docs/api/errors/20001", restStandardException.getType()); + assertEquals("Validation error", restStandardException.getTitle()); + assertEquals(400, (int) restStandardException.getStatus()); + assertEquals("The request contains invalid parameter values.", restStandardException.getDetail()); + assertEquals("/v1/Accounts/AC123/Calls", restStandardException.getInstance()); + assertEquals(20001, (int) restStandardException.getCode()); + + assertNotNull(restStandardException.getErrors()); + assertEquals(2, restStandardException.getErrors().size()); + + assertEquals("The 'From' parameter is required but was not provided.", + restStandardException.getErrors().get(0).getDetail()); + assertEquals("#/From", restStandardException.getErrors().get(0).getPointer()); + + assertEquals("must be a positive integer", restStandardException.getErrors().get(1).getDetail()); + assertEquals("#/age", restStandardException.getErrors().get(1).getPointer()); + } + + @Test + public void fromJsonWithRequiredFieldsOnly() { + final String errorJson = "{\n" + + " \"type\": \"https://www.twilio.com/docs/api/errors/20003\",\n" + + " \"title\": \"Permission denied\",\n" + + " \"status\": 403,\n" + + " \"code\": 20003\n" + + "}"; + + final RestStandardException restStandardException = RestStandardException.fromJson( + new ByteArrayInputStream(errorJson.getBytes()), OBJECT_MAPPER); + + assertEquals("https://www.twilio.com/docs/api/errors/20003", restStandardException.getType()); + assertEquals("Permission denied", restStandardException.getTitle()); + assertEquals(403, (int) restStandardException.getStatus()); + assertEquals(20003, (int) restStandardException.getCode()); + + assertEquals("", restStandardException.getDetail()); + assertEquals("", restStandardException.getInstance()); + + assertNotNull(restStandardException.getErrors()); + assertEquals(0, restStandardException.getErrors().size()); + } +} From 590351a8bcf2b13891a7df5972d88fd88b6490c9 Mon Sep 17 00:00:00 2001 From: sbansla Date: Wed, 4 Feb 2026 13:23:46 +0530 Subject: [PATCH 2/2] removed unused code --- .../com/twilio/exception/ApiException.java | 13 +++++++++ .../exception/RestStandardException.java | 29 ++----------------- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/twilio/exception/ApiException.java b/src/main/java/com/twilio/exception/ApiException.java index 6b73fcaccf..309b966a91 100644 --- a/src/main/java/com/twilio/exception/ApiException.java +++ b/src/main/java/com/twilio/exception/ApiException.java @@ -71,6 +71,19 @@ public ApiException(final RestException restException) { this.details = restException.getDetails(); } + /** + * Create a new API Exception from RFC-9457 Problem Details. + * + * @param restStandardException the rest standard exception + */ + public ApiException(final RestStandardException restStandardException) { + super(restStandardException.getTitle() + ": " + restStandardException.getDetail(), null); + this.code = restStandardException.getCode(); + this.moreInfo = restStandardException.getType(); + this.status = restStandardException.getStatus(); + this.details = null; + } + public Integer getCode() { return code; } diff --git a/src/main/java/com/twilio/exception/RestStandardException.java b/src/main/java/com/twilio/exception/RestStandardException.java index 595c63a3aa..19d1674f6a 100644 --- a/src/main/java/com/twilio/exception/RestStandardException.java +++ b/src/main/java/com/twilio/exception/RestStandardException.java @@ -65,29 +65,7 @@ public String getPointer() { } /** - * Initialize a RFC-9457 Problem Details Exception with required fields - * - * @param type URI reference identifying the problem type - * @param title short, human-readable summary of the problem type - * @param status HTTP status code - * @param code Twilio-specific error code - */ - private RestStandardException( - @JsonProperty("code") final Integer code, - @JsonProperty("status") final Integer status, - @JsonProperty("type") final String type, - @JsonProperty("title") final String title) { - this.type = type; - this.title = title; - this.status = status; - this.code = code; - this.detail = ""; - this.instance = ""; - this.errors = Collections.emptyList(); - } - - /** - * Initialize a RFC-9457 Problem Details Exception. + * Initialize an RFC-9457 Problem Details Exception. * * @param type URI reference identifying the problem type * @param title short, human-readable summary of the problem type @@ -106,14 +84,13 @@ private RestStandardException( @JsonProperty("detail") final String detail, @JsonProperty("instance") final String instance, @JsonProperty("errors") final List errors) { - super(); this.type = type; this.title = title; this.status = status; this.code = code; this.detail = detail == null ? "" : detail; this.instance = instance == null ? "" : instance; - this.errors = errors == null ? Collections.emptyList(): errors; + this.errors = errors == null ? Collections.emptyList() : errors; } /** @@ -190,7 +167,7 @@ public Integer getCode() { /** * Get the array of validation errors for HTTP 400/422 responses. * - * @return list of validation errors, or null if not present + * @return non-null list of validation errors (empty if none are present) */ public List getErrors() { return errors;