Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make rest api return error details on edc exceptions and validation error by default #1729

Merged
merged 1 commit into from
Jul 25, 2022
Merged
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ in the detailed section referring to by linking pull requests or issues.

#### Changed

*
* Return api validation error detail by default (#1492)

#### Removed

Expand Down
13 changes: 6 additions & 7 deletions extensions/http/jersey/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ This extension provides a `Jersey` implementation for the `WebService` service.

## Configuration

| Parameter name | Description | Default value |
|---------------------------------------|---------------------------------------------------------------------------|-----------------------------------------------|
| `edc.web.rest.error.response.verbose` | If true, a detailed response body is sent to the client in case of errors | false |
| `edc.web.rest.cors.enabled` | Enables or disables the CORS filter | false |
| `edc.web.rest.cors.origins` | Defines allowed origins for the CORS filter | "*" |
| `edc.web.rest.cors.headers` | Defines allowed headers for the CORS filter | "origin, content-type, accept, authorization" |
| `edc.web.rest.cors.methods` | Defines allowed methods for the CORS filter | "GET, POST, DELETE, PUT, OPTIONS" |
| Parameter name | Description | Default value |
|---------------------------------------|--------------------------------------------------|-----------------------------------------------|
| `edc.web.rest.cors.enabled` | Enables or disables the CORS filter | false |
| `edc.web.rest.cors.origins` | Defines allowed origins for the CORS filter | "*" |
| `edc.web.rest.cors.headers` | Defines allowed headers for the CORS filter | "origin, content-type, accept, authorization" |
| `edc.web.rest.cors.methods` | Defines allowed methods for the CORS filter | "GET, POST, DELETE, PUT, OPTIONS" |
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@
* Jersey extension configuration class
*/
public class JerseyConfiguration {
@EdcSetting
public static final String ERROR_RESPONSE_VERBOSE_SETTING = "edc.web.rest.error.response.verbose";
@EdcSetting
public static final String CORS_CONFIG_ORIGINS_SETTING = "edc.web.rest.cors.origins";
@EdcSetting
Expand All @@ -35,7 +33,6 @@ public class JerseyConfiguration {
private String allowedHeaders;
private String allowedMethods;
private boolean corsEnabled;
private boolean errorResponseVerbose;

private JerseyConfiguration() {
}
Expand All @@ -50,7 +47,6 @@ public static JerseyConfiguration from(ServiceExtensionContext context) {
config.allowedOrigins = origins;
config.allowedMethods = allowedMethods;
config.corsEnabled = enabled;
config.errorResponseVerbose = context.getSetting(ERROR_RESPONSE_VERBOSE_SETTING, false);

return config;
}
Expand All @@ -75,7 +71,4 @@ public boolean isCorsEnabled() {
return corsEnabled;
}

public boolean isErrorResponseVerbose() {
return errorResponseVerbose;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
import java.util.Map;

import static java.util.stream.Collectors.toSet;
import static org.glassfish.jersey.server.ServerProperties.BV_SEND_ERROR_IN_RESPONSE;
import static org.glassfish.jersey.server.ServerProperties.WADL_FEATURE_DISABLE;

public class JerseyRestService implements WebService {
Expand Down Expand Up @@ -79,17 +78,12 @@ private void registerContext(String contextAlias, List<Object> controllers) {
// Disable WADL as it is not used and emits a warning message about JAXB (which is also not used)
resourceConfig.property(WADL_FEATURE_DISABLE, Boolean.TRUE);

if (configuration.isErrorResponseVerbose()) {
// enable returning jersey bean validation failure detail in body
resourceConfig.property(BV_SEND_ERROR_IN_RESPONSE, Boolean.TRUE);
}

// Register controller (JAX-RS resources) with Jersey. Instances instead of classes are used so extensions may inject them with dependencies and manage their lifecycle.
// In order to use instances with Jersey, the controller types must be registered along with an {@link AbstractBinder} that maps those types to the instances.
resourceConfig.registerClasses(controllers.stream().map(Object::getClass).collect(toSet()));
resourceConfig.registerInstances(new Binder());
resourceConfig.registerInstances(new TypeManagerContextResolver(typeManager));
resourceConfig.registerInstances(new EdcApiExceptionMapper(configuration.isErrorResponseVerbose()));
resourceConfig.registerInstances(new EdcApiExceptionMapper());
resourceConfig.registerInstances(new ValidationExceptionMapper());

if (configuration.isCorsEnabled()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,8 @@

public class EdcApiExceptionMapper implements ExceptionMapper<Throwable> {
private final Map<Class<? extends Throwable>, Response.Status> exceptionMap;
private final boolean verboseResponse;

public EdcApiExceptionMapper(boolean verboseResponse) {
this.verboseResponse = verboseResponse;
public EdcApiExceptionMapper() {
exceptionMap = Map.of(
IllegalArgumentException.class, BAD_REQUEST,
NullPointerException.class, BAD_REQUEST,
Expand All @@ -63,13 +61,17 @@ public Response toResponse(Throwable exception) {

var status = exceptionMap.getOrDefault(exception.getClass(), SERVICE_UNAVAILABLE);

if (exception instanceof EdcApiException && verboseResponse) {
if (exception instanceof EdcApiException) {
var edcApiException = (EdcApiException) exception;
var apiError = ApiErrorDetail.Builder.newInstance()

var errorDetail = ApiErrorDetail.Builder.newInstance()
.message(edcApiException.getMessage())
.type(edcApiException.getType())
.build();
return Response.status(status).entity(List.of(apiError)).build();

return Response.status(status)
.entity(List.of(errorDetail))
.build();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessarily related to this PR. But we do not provide the error details (in the entity part of the Response) is the exception is not of type EdcApiException

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bscholtes1A I think this is correct, as the other exceptions should cause a 5xx error (a server fault, so the client won't need any detail).
I added an issue on this topic: #1730

}

return Response.status(status).build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import jakarta.ws.rs.NotAllowedException;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.NotSupportedException;
import org.eclipse.dataspaceconnector.spi.ApiErrorDetail;
import org.eclipse.dataspaceconnector.spi.exception.AuthenticationFailedException;
import org.eclipse.dataspaceconnector.spi.exception.NotAuthorizedException;
import org.eclipse.dataspaceconnector.spi.exception.ObjectExistsException;
Expand All @@ -33,33 +34,38 @@
import java.util.stream.Stream;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.InstanceOfAssertFactories.LIST;
import static org.assertj.core.api.InstanceOfAssertFactories.type;

class EdcApiExceptionMapperTest {

@ParameterizedTest
@ArgumentsSource(EdcApiExceptions.class)
@ArgumentsSource(JakartaApiExceptions.class)
@ArgumentsSource(JavaExceptions.class)
void toResponseNotVerbose(Throwable throwable, int expectedCode) {
var mapper = new EdcApiExceptionMapper(false);

var response = mapper.toResponse(throwable);

assertThat(response.getStatus()).isEqualTo(expectedCode);
assertThat(response.getStatusInfo().getReasonPhrase()).isNotBlank();
assertThat(response.getEntity()).isNull();
void toResponse_edcApiExceptions(Throwable throwable, int expectedCode) {
var mapper = new EdcApiExceptionMapper();

try (var response = mapper.toResponse(throwable)) {
assertThat(response.getStatus()).isEqualTo(expectedCode);
assertThat(response.getStatusInfo().getReasonPhrase()).isNotBlank();
assertThat(response.getEntity()).asInstanceOf(LIST).first().asInstanceOf(type(ApiErrorDetail.class))
.satisfies(detail -> {
assertThat(detail.getMessage()).isNotBlank();
assertThat(detail.getType()).isNotBlank();
});
}
}

@ParameterizedTest
@ArgumentsSource(EdcApiExceptions.class)
void toResponseVerbose(Throwable throwable, int expectedCode) {
var mapper = new EdcApiExceptionMapper(true);

var response = mapper.toResponse(throwable);
@ArgumentsSource(JakartaApiExceptions.class)
@ArgumentsSource(JavaExceptions.class)
void toResponse_externalExceptions(Throwable throwable, int expectedCode) {
var mapper = new EdcApiExceptionMapper();

assertThat(response.getStatus()).isEqualTo(expectedCode);
assertThat(response.getStatusInfo().getReasonPhrase()).isNotBlank();
assertThat(response.getEntity()).isNotNull();
try (var response = mapper.toResponse(throwable)) {
assertThat(response.getStatus()).isEqualTo(expectedCode);
assertThat(response.getStatusInfo().getReasonPhrase()).isNotBlank();
assertThat(response.getEntity()).isNull();
}
}

private static class EdcApiExceptions implements ArgumentsProvider {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,7 @@ void setUp(EdcExtension extension) {
extension.setConfiguration(Map.of(
"web.http.port", String.valueOf(getFreePort()),
"web.http.test.port", String.valueOf(port),
"web.http.test.path", "/",
"edc.web.rest.error.response.verbose", Boolean.TRUE.toString()
"web.http.test.path", "/"
));
extension.registerSystemExtension(ServiceExtension.class, new MyServiceExtension());
}
Expand Down