Skip to content

Commit

Permalink
Standardize error structure in json responses. (#4170)
Browse files Browse the repository at this point in the history
* Standardize error structure in json responses. Use named types for raising exceptions instead of http status codes peppered throughout the code.

* Whitespace fix

* Rename fix

* Consistent naming for things related to KnownException, for traceability.

* For input field validation errors, list the affected fields as separate json objects. Also uses cleaner helpers for exception mappers.

* Whitespace

* Rename to clarify exception class hierarchy

* Wrap not found responses in a schema and adjusted mapper to match

* Include issue id in comments.
  • Loading branch information
airbyte-jenny committed Jun 22, 2021
1 parent 400d730 commit b3c6505
Show file tree
Hide file tree
Showing 21 changed files with 773 additions and 359 deletions.
280 changes: 183 additions & 97 deletions airbyte-api/src/main/openapi/config.yaml

Large diffs are not rendered by default.

Expand Up @@ -93,7 +93,8 @@
import io.airbyte.scheduler.persistence.JobNotifier;
import io.airbyte.scheduler.persistence.JobPersistence;
import io.airbyte.server.converters.SpecFetcher;
import io.airbyte.server.errors.KnownException;
import io.airbyte.server.errors.BadObjectSchemaKnownException;
import io.airbyte.server.errors.IdNotFoundKnownException;
import io.airbyte.server.handlers.ArchiveHandler;
import io.airbyte.server.handlers.ConnectionsHandler;
import io.airbyte.server.handlers.DestinationDefinitionsHandler;
Expand All @@ -117,7 +118,6 @@
import java.io.File;
import java.io.IOException;
import javax.validation.Valid;
import org.eclipse.jetty.http.HttpStatus;

@javax.ws.rs.Path("/v1")
public class ConfigurationApi implements io.airbyte.api.V1Api {
Expand Down Expand Up @@ -552,12 +552,10 @@ private <T> T execute(HandlerCall<T> call) {
try {
return call.call();
} catch (ConfigNotFoundException e) {
throw new KnownException(
HttpStatus.UNPROCESSABLE_ENTITY_422,
String.format("Could not find configuration for %s: %s.", e.getType().toString(), e.getConfigId()), e);
throw new IdNotFoundKnownException(String.format("Could not find configuration for %s: %s.", e.getType().toString(), e.getConfigId()),
e.getConfigId(), e);
} catch (JsonValidationException e) {
throw new KnownException(
HttpStatus.UNPROCESSABLE_ENTITY_422,
throw new BadObjectSchemaKnownException(
String.format("The provided configuration does not fulfill the specification. Errors: %s", e.getMessage()), e);
} catch (IOException e) {
throw new RuntimeException(e);
Expand Down
@@ -0,0 +1,42 @@
/*
* MIT License
*
* Copyright (c) 2020 Airbyte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package io.airbyte.server.errors;

public class BadObjectSchemaKnownException extends KnownException {

public BadObjectSchemaKnownException(String message) {
super(message);
}

public BadObjectSchemaKnownException(String message, Throwable cause) {
super(message, cause);
}

@Override
public int getHttpCode() {
return 422;
}

}
@@ -0,0 +1,42 @@
/*
* MIT License
*
* Copyright (c) 2020 Airbyte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package io.airbyte.server.errors;

public class ConnectFailureKnownException extends KnownException {

public ConnectFailureKnownException(String message) {
super(message);
}

public ConnectFailureKnownException(String message, Throwable cause) {
super(message, cause);
}

@Override
public int getHttpCode() {
return 400;
}

}
@@ -0,0 +1,70 @@
/*
* MIT License
*
* Copyright (c) 2020 Airbyte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package io.airbyte.server.errors;

import io.airbyte.api.model.NotFoundKnownExceptionInfo;
import org.apache.logging.log4j.core.util.Throwables;

public class IdNotFoundKnownException extends KnownException {

String id;

public IdNotFoundKnownException(String message, String id) {
super(message);
this.id = id;
}

public IdNotFoundKnownException(String message, String id, Throwable cause) {
super(message, cause);
this.id = id;
}

public IdNotFoundKnownException(String message, Throwable cause) {
super(message, cause);
}

@Override
public int getHttpCode() {
return 404;
}

public String getId() {
return id;
}

public NotFoundKnownExceptionInfo getNotFoundKnownExceptionInfo() {
NotFoundKnownExceptionInfo exceptionInfo = new NotFoundKnownExceptionInfo()
.exceptionClassName(this.getClass().getName())
.message(this.getMessage())
.exceptionStack(Throwables.toStringList(this));
if (this.getCause() != null) {
exceptionInfo.rootCauseExceptionClassName(this.getClass().getClass().getName());
exceptionInfo.rootCauseExceptionStack(Throwables.toStringList(this.getCause()));
}
exceptionInfo.id(this.getId());
return exceptionInfo;
}

}
@@ -0,0 +1,42 @@
/*
* MIT License
*
* Copyright (c) 2020 Airbyte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package io.airbyte.server.errors;

public class InternalServerKnownException extends KnownException {

public InternalServerKnownException(String message) {
super(message);
}

public InternalServerKnownException(String message, Throwable cause) {
super(message, cause);
}

@Override
public int getHttpCode() {
return 500;
}

}
Expand Up @@ -24,45 +24,46 @@

package io.airbyte.server.errors;

import com.google.common.collect.ImmutableMap;
import io.airbyte.api.model.InvalidInputExceptionInfo;
import io.airbyte.api.model.InvalidInputProperty;
import io.airbyte.commons.json.Jsons;
import java.util.ArrayList;
import java.util.List;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import org.apache.logging.log4j.core.util.Throwables;

// https://www.baeldung.com/jersey-bean-validation#custom-exception-handler
// handles exceptions related to the request body not matching the openapi config.
@Provider
public class InvalidInputExceptionMapper implements ExceptionMapper<ConstraintViolationException> {

public static InvalidInputExceptionInfo infoFromConstraints(ConstraintViolationException cve) {
InvalidInputExceptionInfo exceptionInfo = new InvalidInputExceptionInfo()
.exceptionClassName(cve.getClass().getName())
.message("Some properties contained invalid input.")
.exceptionStack(Throwables.toStringList(cve));

List<InvalidInputProperty> props = new ArrayList<InvalidInputProperty>();
for (ConstraintViolation<?> cv : cve.getConstraintViolations()) {
props.add(new InvalidInputProperty()
.propertyPath(cv.getPropertyPath().toString())
.message(cv.getMessage())
.invalidValue(cv.getInvalidValue().toString()));
}
exceptionInfo.validationErrors(props);
return exceptionInfo;
}

@Override
public Response toResponse(ConstraintViolationException exception) {
public Response toResponse(ConstraintViolationException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(
Jsons.serialize(
ImmutableMap.of(
"message",
"The received object did not pass validation",
"details",
prepareMessage(exception))))
.entity(Jsons.serialize(InvalidInputExceptionMapper.infoFromConstraints(e)))
.type("application/json")
.build();
}

private String prepareMessage(ConstraintViolationException exception) {
final StringBuilder message = new StringBuilder();
for (ConstraintViolation<?> cv : exception.getConstraintViolations()) {
message.append(
"property: "
+ cv.getPropertyPath()
+ " message: "
+ cv.getMessage()
+ " invalid value: "
+ cv.getInvalidValue());
}
return message.toString();
}

}
Expand Up @@ -25,8 +25,6 @@
package io.airbyte.server.errors;

import com.fasterxml.jackson.core.JsonParseException;
import com.google.common.collect.ImmutableMap;
import io.airbyte.commons.json.Jsons;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
Expand All @@ -37,9 +35,7 @@ public class InvalidJsonExceptionMapper implements ExceptionMapper<JsonParseExce
@Override
public Response toResponse(JsonParseException e) {
return Response.status(422)
.entity(
Jsons.serialize(
ImmutableMap.of("message", "Invalid JSON", "details", e.getOriginalMessage())))
.entity(KnownException.infoFromThrowableWithMessage(e, "Invalid json. " + e.getMessage() + " " + e.getOriginalMessage()))
.type("application/json")
.build();
}
Expand Down
Expand Up @@ -25,7 +25,6 @@
package io.airbyte.server.errors;

import com.fasterxml.jackson.databind.JsonMappingException;
import com.google.common.collect.ImmutableMap;
import io.airbyte.commons.json.Jsons;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
Expand All @@ -38,8 +37,7 @@ public class InvalidJsonInputExceptionMapper implements ExceptionMapper<JsonMapp
public Response toResponse(JsonMappingException e) {
return Response.status(422)
.entity(
Jsons.serialize(
ImmutableMap.of("message", "Invalid JSON", "details", e.getOriginalMessage())))
Jsons.serialize(KnownException.infoFromThrowableWithMessage(e, "Invalid json input. " + e.getMessage() + " " + e.getOriginalMessage())))
.type("application/json")
.build();
}
Expand Down
Expand Up @@ -24,22 +24,39 @@

package io.airbyte.server.errors;

public class KnownException extends RuntimeException {
import io.airbyte.api.model.KnownExceptionInfo;
import org.apache.logging.log4j.core.util.Throwables;

private final int httpCode;
public abstract class KnownException extends RuntimeException {

public KnownException(int httpCode, String message) {
public KnownException(String message) {
super(message);
this.httpCode = httpCode;
}

public KnownException(int httpCode, String message, Throwable cause) {
public KnownException(String message, Throwable cause) {
super(message, cause);
this.httpCode = httpCode;
}

public int getHttpCode() {
return httpCode;
abstract public int getHttpCode();

public KnownExceptionInfo getKnownExceptionInfo() {
return KnownException.infoFromThrowable(this);
}

public static KnownExceptionInfo infoFromThrowableWithMessage(Throwable t, String message) {
KnownExceptionInfo exceptionInfo = new KnownExceptionInfo()
.exceptionClassName(t.getClass().getName())
.message(message)
.exceptionStack(Throwables.toStringList(t));
if (t.getCause() != null) {
exceptionInfo.rootCauseExceptionClassName(t.getClass().getClass().getName());
exceptionInfo.rootCauseExceptionStack(Throwables.toStringList(t.getCause()));
}
return exceptionInfo;
}

public static KnownExceptionInfo infoFromThrowable(Throwable t) {
return infoFromThrowableWithMessage(t, t.getMessage());
}

}

0 comments on commit b3c6505

Please sign in to comment.