Skip to content

Commit

Permalink
Extend error response with additional information (#1325)
Browse files Browse the repository at this point in the history
  • Loading branch information
Hazel-Datastax authored Aug 13, 2024
1 parent 8664579 commit b5402aa
Show file tree
Hide file tree
Showing 9 changed files with 256 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ public interface OperationsConfig {
@WithDefault("100")
int defaultSortPageSize();

/**
* @return Flag to enable vectorization using embedding-gateway.
*/
@WithDefault("false")
boolean enableEmbeddingGateway();

/**
* @return Flag to extend error response with additional information.
*/
@WithDefault("false")
boolean extendError();

/**
* @return Defines the maximum limit of document read to perform in memory sorting <code>10000
* </code>.
Expand Down Expand Up @@ -280,12 +292,6 @@ interface ConsistencyConfig {
@WithDefault("false")
boolean vectorizeEnabled();

/**
* @return Flag to enable vectorization using embedding-gateway.
*/
@WithDefault("false")
boolean enableEmbeddingGateway();

/** Offline mode configuration. */
@NotNull
@Valid
Expand Down
30 changes: 26 additions & 4 deletions src/main/java/io/stargate/sgv2/jsonapi/exception/ErrorCode.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package io.stargate.sgv2.jsonapi.exception;

import io.smallrye.config.SmallRyeConfig;
import io.smallrye.config.SmallRyeConfigBuilder;
import io.stargate.sgv2.jsonapi.config.OperationsConfig;
import io.stargate.sgv2.jsonapi.config.constants.ApiConstants;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.config.ConfigProvider;

/** ErrorCode is our internal enum that provides codes and a default message for that error code. */
public enum ErrorCode {
Expand Down Expand Up @@ -199,6 +204,17 @@ public enum ErrorCode {
TABLE_COLUMN_DEFINITION_MISSING("Column definition is missing for the provided key field"),
TABLE_COLUMN_TYPE_UNSUPPORTED("Unsupported column types");
private final String message;
private final boolean extendError =
ApiConstants.isOffline()
? new SmallRyeConfigBuilder()
.withMapping(OperationsConfig.class)
.build()
.getConfigMapping(OperationsConfig.class)
.extendError()
: ConfigProvider.getConfig()
.unwrap(SmallRyeConfig.class)
.getConfigMapping(OperationsConfig.class)
.extendError();

ErrorCode(String message) {
this.message = message;
Expand All @@ -209,17 +225,23 @@ public String getMessage() {
}

public JsonApiException toApiException(String format, Object... args) {
return new JsonApiException(this, message + ": " + String.format(format, args));
return new JsonApiException(this, getErrorMessage(format, args));
}

public JsonApiException toApiException(
Response.Status httpStatus, String format, Object... args) {
return new JsonApiException(
this, message + ": " + String.format(format, args), null, httpStatus);
return new JsonApiException(this, getErrorMessage(format, args), null, httpStatus);
}

public JsonApiException toApiException(Throwable cause, String format, Object... args) {
return new JsonApiException(this, message + ": " + String.format(format, args), cause);
return new JsonApiException(this, getErrorMessage(format, args), cause);
}

private String getErrorMessage(String format, Object... args) {
if (extendError) {
return String.format(format, args);
}
return message + ": " + String.format(format, args);
}

public JsonApiException toApiException() {
Expand Down
202 changes: 195 additions & 7 deletions src/main/java/io/stargate/sgv2/jsonapi/exception/JsonApiException.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package io.stargate.sgv2.jsonapi.exception;

import static io.stargate.sgv2.jsonapi.exception.ErrorCode.*;

import io.smallrye.config.SmallRyeConfig;
import io.smallrye.config.SmallRyeConfigBuilder;
import io.stargate.sgv2.jsonapi.api.model.command.CommandResult;
import io.stargate.sgv2.jsonapi.config.DebugModeConfig;
import io.stargate.sgv2.jsonapi.config.OperationsConfig;
import io.stargate.sgv2.jsonapi.config.constants.ApiConstants;
import io.stargate.sgv2.jsonapi.exception.mappers.ThrowableToErrorMapper;
import jakarta.ws.rs.core.Response;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.function.Supplier;
import org.eclipse.microprofile.config.ConfigProvider;

Expand All @@ -20,11 +22,82 @@
* directly.
*/
public class JsonApiException extends RuntimeException implements Supplier<CommandResult> {
private final UUID id;

private final ErrorCode errorCode;

private final Response.Status httpStatus;

private final String title;

private final ErrorFamily errorFamily;

private final ErrorScope errorScope;

// some error codes should be classified as "SERVER" family but do not have any pattern
private static final Set<ErrorCode> serverFamily =
new HashSet<>() {
{
add(COUNT_READ_FAILED);
add(CONCURRENCY_FAILURE);
add(TOO_MANY_COLLECTIONS);
add(VECTOR_SEARCH_NOT_AVAILABLE);
add(VECTOR_SEARCH_NOT_SUPPORTED);
add(VECTORIZE_FEATURE_NOT_AVAILABLE);
add(VECTORIZE_SERVICE_NOT_REGISTERED);
add(VECTORIZE_SERVICE_TYPE_UNAVAILABLE);
add(VECTORIZE_INVALID_AUTHENTICATION_TYPE);
add(VECTORIZE_CREDENTIAL_INVALID);
add(VECTORIZECONFIG_CHECK_FAIL);
add(UNAUTHENTICATED_REQUEST);
add(COLLECTION_CREATION_ERROR);
add(INVALID_QUERY);
add(NO_INDEX_ERROR);
}
};

// map of error codes to error scope
private static final Map<Set<ErrorCode>, ErrorScope> errorCodeScopeMap =
Map.of(
new HashSet<>() {
{
add(INVALID_CREATE_COLLECTION_OPTIONS);
add(INVALID_USAGE_OF_VECTORIZE);
add(VECTOR_SEARCH_INVALID_FUNCTION_NAME);
add(VECTOR_SEARCH_TOO_BIG_VALUE);
add(INVALID_PARAMETER_VALIDATION_TYPE);
add(INVALID_ID_TYPE);
add(INVALID_INDEXING_DEFINITION);
}
},
ErrorScope.SCHEMA,
new HashSet<>() {
{
add(INVALID_REQUEST);
add(SERVER_EMBEDDING_GATEWAY_NOT_AVAILABLE);
}
},
ErrorScope.EMBEDDING,
new HashSet<>() {
{
add(ID_NOT_INDEXED);
}
},
ErrorScope.FILTER,
new HashSet<>() {
{
add(VECTOR_SEARCH_USAGE_ERROR);
add(VECTORIZE_USAGE_ERROR);
}
},
ErrorScope.SORT,
new HashSet<>() {
{
add(INVALID_VECTORIZE_VALUE_TYPE);
}
},
ErrorScope.DOCUMENT);

protected JsonApiException(ErrorCode errorCode) {
this(errorCode, errorCode.getMessage(), null);
}
Expand All @@ -45,8 +118,12 @@ protected JsonApiException(ErrorCode errorCode, String message, Throwable cause)
protected JsonApiException(
ErrorCode errorCode, String message, Throwable cause, Response.Status httpStatus) {
super(message, cause);
this.id = UUID.randomUUID();
this.errorCode = errorCode;
this.httpStatus = httpStatus;
this.title = errorCode.getMessage();
this.errorFamily = getErrorFamily();
this.errorScope = getErrorScope(errorFamily);
}

/** {@inheritDoc} */
Expand Down Expand Up @@ -83,11 +160,31 @@ public CommandResult.Error getCommandResultError(String message, Response.Status
}
DebugModeConfig debugModeConfig = config.getConfigMapping(DebugModeConfig.class);
final boolean debugEnabled = debugModeConfig.enabled();
final Map<String, Object> fields =
debugEnabled
? Map.of(
"errorCode", errorCode.name(), "exceptionClass", this.getClass().getSimpleName())
: Map.of("errorCode", errorCode.name());
final boolean extendError = config.getConfigMapping(OperationsConfig.class).extendError();
Map<String, Object> fields = null;

if (extendError) {
fields =
new HashMap<String, Object>(
Map.of(
"id",
id,
"errorCode",
errorCode.name(),
"family",
errorFamily,
"scope",
errorScope,
"title",
title));
} else {
fields = new HashMap<String, Object>(Map.of("errorCode", errorCode.name()));
}

if (debugEnabled) {
fields.put("exceptionClass", this.getClass().getSimpleName());
}

return new CommandResult.Error(message, fieldsForMetricsTag, fields, status);
}

Expand All @@ -106,4 +203,95 @@ public ErrorCode getErrorCode() {
public Response.Status getHttpStatus() {
return httpStatus;
}

private ErrorFamily getErrorFamily() {
if (serverFamily.contains(errorCode)
|| errorCode.name().startsWith("SERVER")
|| errorCode.name().startsWith("EMBEDDING")) {
return ErrorFamily.SERVER;
}
return ErrorFamily.REQUEST;
}

private ErrorScope getErrorScope(ErrorFamily family) {
// first handle special cases
if (errorCode == SERVER_INTERNAL_ERROR) {
return ErrorScope.EMPTY;
}
for (Map.Entry<Set<ErrorCode>, ErrorScope> entry : errorCodeScopeMap.entrySet()) {
if (entry.getKey().contains(errorCode)) {
return entry.getValue();
}
}

// decide the scope based in error code pattern
if (errorCode.name().contains("SCHEMA")) {
return ErrorScope.SCHEMA;
}
if (errorCode.name().startsWith("EMBEDDING") || errorCode.name().startsWith("VECTORIZE")) {
return ErrorScope.EMBEDDING;
}
if (errorCode.name().contains("FILTER")) {
return ErrorScope.FILTER;
}
if (errorCode.name().contains("SORT")) {
return ErrorScope.SORT;
}
if (errorCode.name().contains("INDEX")) {
return ErrorScope.INDEX;
}
if (errorCode.name().contains("UPDATE")) {
return ErrorScope.UPDATE;
}
if (errorCode.name().contains("SHRED") || errorCode.name().contains("DOCUMENT")) {
return ErrorScope.DOCUMENT;
}
if (errorCode.name().contains("PROJECTION")) {
return ErrorScope.PROJECTION;
}
if (errorCode.name().contains("AUTHENTICATION")) {
return ErrorScope.AUTHENTICATION;
}
if (errorCode.name().contains("OFFLINE")) {
return ErrorScope.DATA_LOADER;
}

// decide the scope based on family
if (family == ErrorFamily.SERVER) {
return ErrorScope.DATABASE;
}

return ErrorScope.EMPTY;
}

enum ErrorFamily {
SERVER,
REQUEST
}

enum ErrorScope {
AUTHENTICATION("AUTHENTICATION"),
DATA_LOADER("DATA_LOADER"),
DATABASE("DATABASE"),
DOCUMENT("DOCUMENT"),
EMBEDDING("EMBEDDING"),
EMPTY(""),
FILTER("FILTER"),
INDEX("INDEX"),
PROJECTION("PROJECTION"),
SCHEMA("SCHEMA"),
SORT("SORT"),
UPDATE("UPDATE");

private final String scope;

ErrorScope(String scope) {
this.scope = scope;
}

@Override
public String toString() {
return this.scope;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusIntegrationTest;
import io.restassured.http.ContentType;
import io.stargate.sgv2.jsonapi.exception.ErrorCode;
import io.stargate.sgv2.jsonapi.testresource.DseTestResource;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.jupiter.api.*;
Expand Down Expand Up @@ -174,7 +173,7 @@ public void duplicateNonVectorCollectionName() {
.body("status", is(nullValue()))
.body("data", is(nullValue()))
.body("errors[0].exceptionClass", is("JsonApiException"))
.body("errors[0].errorCode", is(ErrorCode.EXISTING_COLLECTION_DIFFERENT_SETTINGS.name()))
.body("errors[0].errorCode", is("EXISTING_COLLECTION_DIFFERENT_SETTINGS"))
.body(
"errors[0].message",
containsString(
Expand Down Expand Up @@ -216,7 +215,7 @@ public void duplicateVectorCollectionName() {
.body("status", is(nullValue()))
.body("data", is(nullValue()))
.body("errors[0].exceptionClass", is("JsonApiException"))
.body("errors[0].errorCode", is(ErrorCode.EXISTING_COLLECTION_DIFFERENT_SETTINGS.name()))
.body("errors[0].errorCode", is("EXISTING_COLLECTION_DIFFERENT_SETTINGS"))
.body(
"errors[0].message",
containsString(
Expand Down Expand Up @@ -249,7 +248,7 @@ public void duplicateVectorCollectionNameWithDiffSetting() {
.body("status", is(nullValue()))
.body("data", is(nullValue()))
.body("errors[0].exceptionClass", is("JsonApiException"))
.body("errors[0].errorCode", is(ErrorCode.EXISTING_COLLECTION_DIFFERENT_SETTINGS.name()))
.body("errors[0].errorCode", is("EXISTING_COLLECTION_DIFFERENT_SETTINGS"))
.body(
"errors[0].message",
containsString(
Expand All @@ -267,7 +266,7 @@ public void duplicateVectorCollectionNameWithDiffSetting() {
.body("status", is(nullValue()))
.body("data", is(nullValue()))
.body("errors[0].exceptionClass", is("JsonApiException"))
.body("errors[0].errorCode", is(ErrorCode.EXISTING_COLLECTION_DIFFERENT_SETTINGS.name()))
.body("errors[0].errorCode", is("EXISTING_COLLECTION_DIFFERENT_SETTINGS"))
.body(
"errors[0].message",
containsString(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusIntegrationTest;
import io.restassured.http.ContentType;
import io.stargate.sgv2.jsonapi.exception.ErrorCode;
import io.stargate.sgv2.jsonapi.testresource.DseTestResource;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.ClassOrderer;
Expand Down Expand Up @@ -1713,8 +1712,7 @@ public void trySetWithInvalidDateField() {
.body(
"errors[0].message",
is(
ErrorCode.SHRED_BAD_EJSON_VALUE.getMessage()
+ ": Date ($date) needs to have NUMBER value, has STRING (path 'createdAt')"));
"Bad JSON Extension value: Date ($date) needs to have NUMBER value, has STRING (path 'createdAt')"));
}
}

Expand Down
Loading

0 comments on commit b5402aa

Please sign in to comment.