Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/main/java/com/twilio/exception/ApiException.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
175 changes: 175 additions & 0 deletions src/main/java/com/twilio/exception/RestStandardException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
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<ValidationError> 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 an 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<ValidationError> errors) {
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 non-null list of validation errors (empty if none are present)
*/
public List<ValidationError> getErrors() {
return errors;
}
}
80 changes: 80 additions & 0 deletions src/test/java/com/twilio/exception/RestStandardExceptionTest.java
Original file line number Diff line number Diff line change
@@ -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());
}
}