CosmosException.java
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.cosmos;
import com.azure.core.exception.AzureException;
import com.azure.cosmos.implementation.Constants;
import com.azure.cosmos.implementation.CosmosError;
import com.azure.cosmos.implementation.HttpConstants;
import com.azure.cosmos.implementation.RequestTimeline;
import com.azure.cosmos.implementation.Utils;
import com.azure.cosmos.implementation.apachecommons.lang.StringUtils;
import com.azure.cosmos.implementation.directconnectivity.Uri;
import com.azure.cosmos.implementation.directconnectivity.rntbd.RntbdChannelAcquisitionTimeline;
import com.azure.cosmos.implementation.directconnectivity.rntbd.RntbdEndpointStatistics;
import com.azure.cosmos.models.ModelBridgeInternal;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import static com.azure.cosmos.CosmosDiagnostics.USER_AGENT_KEY;
/**
* This class defines a custom exception type for all operations on
* CosmosClient in the Azure Cosmos DB database service. Applications are
* expected to catch CosmosException and handle errors as appropriate when
* calling methods on CosmosClient.
* <p>
* Errors coming from the service during normal execution are converted to
* CosmosException before returning to the application with the following
* exception:
* <p>
* When a BE error is encountered during a QueryIterable<T> iteration, an
* IllegalStateException is thrown instead of CosmosException.
* <p>
* When a transport level error happens that request is not able to reach the
* service, an IllegalStateException is thrown instead of CosmosException.
*/
public class CosmosException extends AzureException {
private static final long serialVersionUID = 1L;
private static final ObjectMapper mapper = new ObjectMapper();
private final static String USER_AGENT = Utils.getUserAgent();
/**
* Status code
*/
private final int statusCode;
/**
* Response headers
*/
private final Map<String, String> responseHeaders;
/**
* Cosmos diagnostics
*/
private CosmosDiagnostics cosmosDiagnostics;
/**
* Request timeline
*/
private RequestTimeline requestTimeline;
/**
* Channel acquisition timeline
*/
private RntbdChannelAcquisitionTimeline channelAcquisitionTimeline;
/**
* Cosmos error
*/
private CosmosError cosmosError;
/**
* RNTBD channel task queue size
*/
private int rntbdChannelTaskQueueSize;
/**
* RNTBD endpoint statistics
*/
private RntbdEndpointStatistics rntbdEndpointStatistics;
/**
* LSN
*/
long lsn;
/**
* Partition key range ID
*/
String partitionKeyRangeId;
/**
* Request headers
*/
Map<String, String> requestHeaders;
/**
* Request URI
*/
Uri requestUri;
/**
* Resource address
*/
String resourceAddress;
/**
* Request payload length
*/
private int requestPayloadLength;
/**
* RNTBD pending request queue size
*/
private int rntbdPendingRequestQueueSize;
/**
* RNTBD request length
*/
private int rntbdRequestLength;
/**
* RNTBD response length
*/
private int rntbdResponseLength;
/**
* Sending request has started
*/
private boolean sendingRequestHasStarted;
/**
* Creates a new instance of the CosmosException class.
*
* @param statusCode the http status code of the response.
* @param message the string message.
* @param responseHeaders the response headers.
* @param cause the inner exception
*/
protected CosmosException(int statusCode, String message, Map<String, String> responseHeaders, Throwable cause) {
super(message, cause);
this.statusCode = statusCode;
this.responseHeaders = new ConcurrentHashMap<>();
if (responseHeaders != null) {
for (Map.Entry<String, String> entry: responseHeaders.entrySet()) {
if (entry.getKey() != null && entry.getValue() != null) {
this.responseHeaders.put(entry.getKey(), entry.getValue());
}
}
}
}
/**
* Creates a new instance of the CosmosException class.
*
* @param statusCode the http status code of the response.
* @param errorMessage the error message.
*/
protected CosmosException(int statusCode, String errorMessage) {
this(statusCode, errorMessage, null, null);
this.cosmosError = new CosmosError();
ModelBridgeInternal.setProperty(cosmosError, Constants.Properties.MESSAGE, errorMessage);
}
/**
* Creates a new instance of the CosmosException class.
*
* @param statusCode the http status code of the response.
* @param innerException the original exception.
*/
protected CosmosException(int statusCode, Exception innerException) {
this(statusCode, null, null, innerException);
}
/**
* Creates a new instance of the CosmosException class.
*
* @param statusCode the http status code of the response.
* @param cosmosErrorResource the error resource object.
* @param responseHeaders the response headers.
*/
protected CosmosException(int statusCode, CosmosError cosmosErrorResource, Map<String, String> responseHeaders) {
this(/* resourceAddress */ null, statusCode, cosmosErrorResource, responseHeaders);
}
/**
* Creates a new instance of the CosmosException class.
*
* @param resourceAddress the address of the resource the request is associated with.
* @param statusCode the http status code of the response.
* @param cosmosErrorResource the error resource object.
* @param responseHeaders the response headers.
*/
protected CosmosException(String resourceAddress,
int statusCode,
CosmosError cosmosErrorResource,
Map<String, String> responseHeaders) {
this(statusCode, cosmosErrorResource == null ? null : cosmosErrorResource.getMessage(), responseHeaders, null);
this.resourceAddress = resourceAddress;
this.cosmosError = cosmosErrorResource;
}
/**
* Creates a new instance of the CosmosException class.
*
* @param resourceAddress the address of the resource the request is associated with.
* @param statusCode the http status code of the response.
* @param cosmosErrorResource the error resource object.
* @param responseHeaders the response headers.
* @param cause the inner exception
*/
protected CosmosException(String resourceAddress,
int statusCode,
CosmosError cosmosErrorResource,
Map<String, String> responseHeaders,
Throwable cause) {
this(statusCode, cosmosErrorResource == null ? null : cosmosErrorResource.getMessage(), responseHeaders, cause);
this.resourceAddress = resourceAddress;
this.cosmosError = cosmosErrorResource;
}
/**
* Creates a new instance of the CosmosException class.
*
* @param message the string message.
* @param statusCode the http status code of the response.
* @param exception the exception object.
* @param responseHeaders the response headers.
* @param resourceAddress the address of the resource the request is associated with.
*/
protected CosmosException(String message, Exception exception, Map<String, String> responseHeaders, int statusCode,
String resourceAddress) {
this(statusCode, message, responseHeaders, exception);
this.resourceAddress = resourceAddress;
}
@Override
public String getMessage() {
try {
ObjectNode messageNode = mapper.createObjectNode();
messageNode.put("innerErrorMessage", innerErrorMessage());
if (cosmosDiagnostics != null) {
cosmosDiagnostics.fillCosmosDiagnostics(messageNode, null);
}
return mapper.writeValueAsString(messageNode);
} catch (JsonProcessingException e) {
if (cosmosDiagnostics == null) {
return innerErrorMessage();
}
return innerErrorMessage() + ", " + cosmosDiagnostics.toString();
}
}
/**
* Gets the activity ID associated with the request.
*
* @return the activity ID.
*/
public String getActivityId() {
if (this.responseHeaders != null) {
return this.responseHeaders.get(HttpConstants.HttpHeaders.ACTIVITY_ID);
}
return null;
}
/**
* Gets the http status code.
*
* @return the status code.
*/
public int getStatusCode() {
return this.statusCode;
}
/**
* Gets the sub status code.
*
* @return the status code.
*/
public int getSubStatusCode() {
int code = HttpConstants.SubStatusCodes.UNKNOWN;
if (this.responseHeaders != null) {
String subStatusString = this.responseHeaders.get(HttpConstants.HttpHeaders.SUB_STATUS);
if (StringUtils.isNotEmpty(subStatusString)) {
try {
code = Integer.parseInt(subStatusString);
} catch (NumberFormatException e) {
// If value cannot be parsed as Integer, return Unknown.
}
}
}
return code;
}
void setSubStatusCode(int subStatusCode) {
this.responseHeaders.put(HttpConstants.HttpHeaders.SUB_STATUS, Integer.toString(subStatusCode));
}
/**
* Gets the error code associated with the exception.
*
* @return the error.
*/
CosmosError getError() {
return this.cosmosError;
}
void setError(CosmosError cosmosError) {
this.cosmosError = cosmosError;
}
/**
* Gets the recommended time duration after which the client can retry failed
* requests
*
* @return the recommended time duration after which the client can retry failed
* requests.
*/
public Duration getRetryAfterDuration() {
long retryIntervalInMilliseconds = 0;
if (this.responseHeaders != null) {
String header = this.responseHeaders.get(HttpConstants.HttpHeaders.RETRY_AFTER_IN_MILLISECONDS);
if (StringUtils.isNotEmpty(header)) {
try {
retryIntervalInMilliseconds = Long.parseLong(header);
} catch (NumberFormatException e) {
// If the value cannot be parsed as long, return 0.
}
}
}
//
// In the absence of explicit guidance from the backend, don't introduce
// any unilateral retry delays here.
return Duration.ofMillis(retryIntervalInMilliseconds);
}
/**
* Gets the response headers as key-value pairs
*
* @return the response headers
*/
public Map<String, String> getResponseHeaders() {
return this.responseHeaders;
}
/**
* Gets the resource address associated with this exception.
*
* @return the resource address associated with this exception.
*/
String getResourceAddress() {
return this.resourceAddress;
}
/**
* Gets the Cosmos Diagnostic Statistics associated with this exception.
*
* @return Cosmos Diagnostic Statistics associated with this exception.
*/
public CosmosDiagnostics getDiagnostics() {
return cosmosDiagnostics;
}
CosmosException setDiagnostics(CosmosDiagnostics cosmosDiagnostics) {
this.cosmosDiagnostics = cosmosDiagnostics;
return this;
}
/**
* Gets the request charge as request units (RU) consumed by the operation.
* <p>
* For more information about the RU and factors that can impact the effective charges please visit
* <a href="https://docs.microsoft.com/en-us/azure/cosmos-db/request-units">Request Units in Azure Cosmos DB</a>
*
* @return the request charge.
*/
public double getRequestCharge() {
String value = this.getResponseHeaders().get(HttpConstants.HttpHeaders.REQUEST_CHARGE);
if (StringUtils.isEmpty(value)) {
return 0;
}
return Double.parseDouble(value);
}
@Override
public String toString() {
try {
ObjectNode exceptionMessageNode = mapper.createObjectNode();
exceptionMessageNode.put("ClassName", getClass().getSimpleName());
exceptionMessageNode.put(USER_AGENT_KEY, USER_AGENT);
exceptionMessageNode.put("statusCode", statusCode);
exceptionMessageNode.put("resourceAddress", resourceAddress);
if (cosmosError != null) {
exceptionMessageNode.put("error", cosmosError.toJson());
}
exceptionMessageNode.put("innerErrorMessage", innerErrorMessage());
exceptionMessageNode.put("causeInfo", causeInfo());
if (responseHeaders != null) {
exceptionMessageNode.put("responseHeaders", responseHeaders.toString());
}
List<Map.Entry<String, String>> filterRequestHeaders = filterSensitiveData(requestHeaders);
if (filterRequestHeaders != null) {
exceptionMessageNode.put("requestHeaders", filterRequestHeaders.toString());
}
if(this.cosmosDiagnostics != null) {
cosmosDiagnostics.fillCosmosDiagnostics(exceptionMessageNode, null);
}
return mapper.writeValueAsString(exceptionMessageNode);
} catch (JsonProcessingException ex) {
return getClass().getSimpleName() + "{" + USER_AGENT_KEY +"=" + USER_AGENT + ", error=" + cosmosError + ", " +
"resourceAddress='"
+ resourceAddress + ", statusCode=" + statusCode + ", message=" + getMessage()
+ ", causeInfo=" + causeInfo() + ", responseHeaders=" + responseHeaders + ", requestHeaders="
+ filterSensitiveData(requestHeaders) + '}';
}
}
String innerErrorMessage() {
String innerErrorMessage = super.getMessage();
if (cosmosError != null) {
innerErrorMessage = cosmosError.getMessage();
if (innerErrorMessage == null) {
innerErrorMessage = String.valueOf(
ModelBridgeInternal.getObjectFromJsonSerializable(cosmosError, "Errors"));
}
}
return innerErrorMessage;
}
private String causeInfo() {
Throwable cause = getCause();
if (cause != null) {
return String.format("[class: %s, message: %s]", cause.getClass(), cause.getMessage());
}
return null;
}
private List<Map.Entry<String, String>> filterSensitiveData(Map<String, String> requestHeaders) {
if (requestHeaders == null) {
return null;
}
return requestHeaders.entrySet().stream().filter(entry -> !HttpConstants.HttpHeaders.AUTHORIZATION.equalsIgnoreCase(entry.getKey()))
.collect(Collectors.toList());
}
RequestTimeline getRequestTimeline() {
return this.requestTimeline;
}
void setRequestTimeline(RequestTimeline requestTimeline) {
this.requestTimeline = requestTimeline;
}
RntbdChannelAcquisitionTimeline getChannelAcquisitionTimeline() {
return this.channelAcquisitionTimeline;
}
void setChannelAcquisitionTimeline(RntbdChannelAcquisitionTimeline channelAcquisitionTimeline) {
this.channelAcquisitionTimeline = channelAcquisitionTimeline;
}
void setResourceAddress(String resourceAddress) {
this.resourceAddress = resourceAddress;
}
void setRntbdServiceEndpointStatistics(RntbdEndpointStatistics rntbdEndpointStatistics) {
this.rntbdEndpointStatistics = rntbdEndpointStatistics;
}
RntbdEndpointStatistics getRntbdServiceEndpointStatistics() {
return this.rntbdEndpointStatistics;
}
void setRntbdRequestLength(int rntbdRequestLength) {
this.rntbdRequestLength = rntbdRequestLength;
}
int getRntbdRequestLength() {
return this.rntbdRequestLength;
}
void setRntbdResponseLength(int rntbdResponseLength) {
this.rntbdResponseLength = rntbdResponseLength;
}
int getRntbdResponseLength() {
return this.rntbdResponseLength;
}
void setRequestPayloadLength(int requestBodyLength) {
this.requestPayloadLength = requestBodyLength;
}
int getRequestPayloadLength() {
return this.requestPayloadLength;
}
boolean hasSendingRequestStarted() {
return this.sendingRequestHasStarted;
}
void setSendingRequestHasStarted(boolean hasSendingRequestStarted) {
this.sendingRequestHasStarted = hasSendingRequestStarted;
}
int getRntbdChannelTaskQueueSize() {
return this.rntbdChannelTaskQueueSize;
}
void setRntbdChannelTaskQueueSize(int rntbdChannelTaskQueueSize) {
this.rntbdChannelTaskQueueSize = rntbdChannelTaskQueueSize;
}
int getRntbdPendingRequestQueueSize() {
return this.rntbdChannelTaskQueueSize;
}
void setRntbdPendingRequestQueueSize(int rntbdPendingRequestQueueSize) {
this.rntbdPendingRequestQueueSize = rntbdPendingRequestQueueSize;
}
}