Backstopper - Keep Your API Errors in the Field of Play

Download Code Coverage

Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater.

TL;DR

Backstopper guarantees that a consistent error contract which you can define will be sent to callers no matter the source of the error when properly integrated into your API framework. No chance of undesired information leaking to the caller like stack traces, and callers can always rely on an error contract they can programmatically process.

If you prefer hands-on exploration rather than readmes and user guides, the sample applications provide concrete examples of using Backstopper that are simple, compact, and straightforward.

The rest of this readme is intended to get you oriented and running quickly, and the User Guide contains more in-depth details.

Additional Features Overview

Barebones Example (assumes framework integration is already done)

1. Define your API's errors
public enum MyProjectError implements ApiError {
    // Constructor args for this example are: errorCode, message, httpStatusCode
    EMAIL_CANNOT_BE_EMPTY(42, "Email must be specified", 400),
    INVALID_EMAIL_ADDRESS(43, "Invalid email address format", 400),
    // -- SNIP -- 
}
2a. Use JSR 303 Bean Validation to define object validation (optional)
public class Payload {
    @NotEmpty(message = "EMAIL_CANNOT_BE_EMPTY")
    @Email(message = "INVALID_EMAIL_ADDRESS")
    private String email;
    // -- SNIP -- 
}
--AND/OR--
2b. Throw errors manually anytime (doesn't have to be just for validation)
throw ApiException.newBuilder()
                  .withApiErrors(MyProjectError.INVALID_EMAIL_ADDRESS)
                  .withExceptionMessage("Error validating email field.")
                  .withExtraResponseHeaders(Pair.of("received-email", singletonList(payload.email)))
                  .withExtraDetailsForLogging(Pair.of("received_email", payload.email))
                  .build();
3. Send payload that will cause errors to be thrown

POST /profile

{
    "email": "[email protected]@[email protected]",
    "etc": "-- SNIP --"
}
4. Receive expected error response

HTTP Status Code: 400 Bad Request

{
  "error_id": "408d516f-68d1-41f3-adb1-b3dc0affaaf2",
  "errors": [
    {
      "code": 43,
      "message": "Invalid email address format",
      "metadata": {
        "field": "email"
      }
    }
  ]
}
5. Use the error_id to locate debugging details in the logs (a 5xx error would also include the stack trace)
2016-09-21_12:14:00.620 |-WARN  c.n.b.h.j.Jersey1ApiExceptionHandler - ApiExceptionHandlerBase handled 
↪exception occurred: error_uid=408d516f-68d1-41f3-adb1-b3dc0affaaf2, 
↪exception_class=com.nike.backstopper.exception.ClientDataValidationError, returned_http_status_code=400, 
↪contributing_errors="INVALID_EMAIL_ADDRESS", request_uri="/profile", request_method="POST", 
↪query_string="null", request_headers="Host=localhost:8080, --snip-- Content-Type=application/json",
↪client_data_validation_failed_objects="com.myorg.Payload", 
↪constraint_violation_details="Payload.email|org.hibernate.validator.constraints.Email|INVALID_EMAIL_ADDRESS"

General-Purpose Modules

Framework-Specific Modules

Framework Integration Sample Applications

Note that the sample apps are an excellent source for framework integration examples, but they are also very helpful for giving you an overview and exploring what you can do with Backstopper regardless of framework: how to create and throw errors, how they show up for the caller in the response, what Backstopper outputs in the application logs when errors occur, and how to find the relevant log message given a specific error response. The VerifyExpectedErrorsAreReturnedComponentTest component tests in the sample apps exercise a large portion of Backstopper's functionality - you can learn a lot by running that component test, seeing what the sample app returns in the error responses, and exploring the associated endpoints and framework configuration in the sample apps to see how it all fits together.

Quickstart

Getting started is a matter of integrating Backstopper into your project and learning how to use its features. The following sections will help guide you in getting started, and the sample applications should be consulted for concrete examples.

Quickstart - Integration

The first thing to do is see if there is a framework integration plugin library already created for the framework you're using. If so then refer to that framework-specific library's readme as well as its sample project to learn how to integrate Backstopper into your project.

If a framework-specific plugin library does not already exist for your project then you'll need to create your own integration. If the result is potentially reusable for others using the same framework then please consider contributing it back to the Backstopper project so others can benefit! The new framework integrations section has full details, and in particular the pseudo-code section should give you a quick idea of what is required.

IMPORTANT NOTE: Your project integration should not be considered complete until you have added and enabled the reusable unit tests that enforce Backstopper rules and conventions. See the Reusable Unit Tests for Enforcing Backstopper Rules and Conventions section of the User Guide for information on setting these up.

Quickstart - Usage

Once your project is properly integrated with Backstopper a large portion of errors should be handled for you (framework errors, errors resulting from validation of incoming payloads, etc), however for most API projects you'll need to throw errors or interact with Backstopper in other situations. Again, the sample projects are excellent for showing how this is done in practice, but here are a few common use cases and how to solve them:

Defining a set of ApiErrors

Defining groups of ApiErrors as enums has proven to be a useful pattern. Normally you'd want to break ApiErrors out into a group of "core errors" that you could share with projects across your organization (see SampleCoreApiError for an example) and different sets of ApiErrors for each individual project (see any of the sample application SampleProjectApiError classes for an example). For the purpose of this example here is a mishmash showing how to define an enum of errors with different properties (basic code/message/http-status errors, "mirror" errors, and errors with metadata):

public enum MyProjectApiError implements ApiError {
    GENERIC_SERVICE_ERROR(10, "An error occurred while fulfilling the request", 500),
    // Mirrors GENERIC_SERVICE_ERROR for the caller, but will show up in the logs with a different name
    SOME_OTHER_SERVICE_ERROR(GENERIC_SERVICE_ERROR),
    GENERIC_BAD_REQUEST(20, "Invalid request", 400),
    // Includes metadata in the response payload sent to the caller
    SOME_OTHER_BAD_REQUEST(30, "You failed to pass the required foo", 400,
                           MapBuilder.builder("missing_field", (Object)"foo").build()),
    // Also a mirror for another ApiError, but includes extra metadata that will show up in the response
    YET_ANOTHER_BAD_REQUEST(GENERIC_BAD_REQUEST, MapBuilder.builder("field", (Object)"bar").build());

    private final ApiError delegate;

    MyProjectApiError(ApiError delegate) { this.delegate = delegate; }

    MyProjectApiError(ApiError delegate, Map<String, Object> additionalMetadata) {
        this(new ApiErrorWithMetadata(delegate, additionalMetadata));
    }

    MyProjectApiError(int errorCode, String message, int httpStatusCode) {
        this(errorCode, message, httpStatusCode, null);
    }

    MyProjectApiError(int errorCode, String message, int httpStatusCode, Map<String, Object> metadata) {
        this(new ApiErrorBase(
            "delegated-to-enum-name-" + UUID.randomUUID().toString(), errorCode, message, httpStatusCode, 
            metadata
        ));
    }

    @Override
    public String getName() { return this.name(); }

    @Override
    public String getErrorCode() { return delegate.getErrorCode(); }

    @Override
    public String getMessage() { return delegate.getMessage(); }

    @Override
    public int getHttpStatusCode() { return delegate.getHttpStatusCode(); }

    @Override
    public Map<String, Object> getMetadata() { return delegate.getMetadata(); }

}  

Defining a ProjectApiErrors for your project

Backstopper needs a ProjectApiErrors defined for each project in order to work. If possible you should create an abstract base class that is setup with the core errors for your organization - see SampleProjectApiErrorsBase for an example. Then each individual project would simply need to extend the base class and fill in the project-specific set of ApiErrors and the error range it's using. See any of the sample application SampleProjectApiErrorsImpl classes for an example. The javadocs for ProjectApiErrors contains in-depth information as well.

Manually throwing an arbitrary error with full control over the resulting error contract, response headers, and logging info
// The only requirement is that you have at least one ApiError. Everything else is optional.
throw ApiException.newBuilder()
                  .withApiErrors(MyProjectApiError.FOO_ERROR, MyProjectApiError.BAD_THING_HAPPENED)
                  .withExceptionMessage("Useful message for exception in the logs")
                  .withExceptionCause(originalCause)
                  .withExtraResponseHeaders(
                      Pair.of("useful-single-header-for-caller", singletonList("thing1")),
                      Pair.of("also-useful-multivalue-header", Arrays.asList("thing2", "thing3"))
                  )
                  .withExtraDetailsForLogging(
                      Pair.of("important_info", "foo"),
                      Pair.of("also_important", "bar")
                  )
                  .build();

Creating a custom ApiExceptionHandlerListener to handle a typed exception

This is only really necessary if you can't (or don't want to) throw an ApiException and need Backstopper to properly handle a typed exception it wouldn't otherwise know about. Many projects never need to do this.

public static class MyFrameworkExceptionHandlerListener implements ApiExceptionHandlerListener {
    @Override
    public ApiExceptionHandlerListenerResult shouldHandleException(Throwable ex) {
        if (ex instanceof MyFrameworkException) {
            // The exception is a MyFrameworkException, so this listener should handle it.
            MyFrameworkException myEx = (MyFrameworkException)ex;
            SortedApiErrorSet apiErrors =
                SortedApiErrorSet.singletonSortedSetOf(MyProjectApiError.SOME_OTHER_BAD_REQUEST);
            List<Pair<String, String>> extraDetailsForLogging = Arrays.asList(
                Pair.of("important_foo_info", myEx.foo()),
                Pair.of("important_bar_info", myEx.bar())
            );
            List<Pair<String, List<String>>> extraResponseHeaders = Arrays.asList(
                Pair.of("foo-info", myEx.foo()),
                Pair.of("bar-info", myEx.bar())
            );
            return ApiExceptionHandlerListenerResult.handleResponse(
                apiErrors, extraDetailsForLogging, extraResponseHeaders
            );
        }

        // The exception wasn't a MyFrameworkException, so this listener should ignore it.
        return ApiExceptionHandlerListenerResult.ignoreResponse();
    }
}

After defining a new ApiExceptionHandlerListener you'll need to register it with the ApiExceptionHandlerBase running your Backstopper system. This is a procedure that is often different for each framework integration.

User Guide

For further details please consult the User Guide.

License

Backstopper is released under the Apache License, Version 2.0