Skip to content

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

License

Notifications You must be signed in to change notification settings

nicmunroe/backstopper

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Backstopper - Keep Your API Errors in the Field of Play

Maven Central Build Code Coverage License

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

(NOTE: The Backstopper 1.x branch contains a version of Backstopper for Java 7+, and for the javax ecosystem. The current Backstopper supports Java 17+ and the jakarta ecosystem. The Backstopper 1.x releases also contain support for JAX-RS 2, Jersey 1 and 2, Spring 4 and 5, and Springboot 1 and 2 - see here.)

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.

Table of Contents

Additional Features Overview

  • Backstopper trivializes the task of creating and throwing errors that will be sent to the caller with the desired error contract, and allows you to gather all of your project-specific API error definitions in one place (e.g. as an enum) for easy access, modification, and addition.
  • All errors are logged with full request context and error debugging info along with a UUID. The caller is sent the same UUID in the error response payload so you can instantly connect an individual error response with the application log message containing full debugging details. This makes the usual tedious process of debugging why an API error was shown to the caller simple and straightforward.
  • Includes optional support for defining a common set of "core API errors" that can be shared via jar library so that all projects in your organization use the same set of API errors for common error cases. Individual projects are then free to focus on their project-specific API errors.
  • Contains optional integration support for Java's JSR 303 Bean Validation system (@NotNull, @Max, etc) so that validation errors occurring due to invalid payloads sent by callers (among other use cases) will be automatically converted to your predefined set of project API errors.
  • Integration libraries are included for several API frameworks already, and adding support for other frameworks is a relatively straightforward process. Backstopper is geared primarily towards HTTP-based APIs, but could be used in other circumstances if desired and without polluting your project with unwanted HTTP API dependencies.

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": "not@a@valid@email",
	"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.s.SpringApiExceptionHandler - 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"

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, Map.of("missing_field", "foo")),
    // Also a mirror for another ApiError, but includes extra metadata that will show up in the response
    YET_ANOTHER_BAD_REQUEST(GENERIC_BAD_REQUEST, Map.of("field", "bar"));

    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(), 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 include 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();

Above is a "kitchen sink" example showing all the options available when throwing an ApiException manually. For automatic input validation and error handling see the User Guide's section on Bean Validation.

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 class MyFrameworkExceptionHandlerListener implements ApiExceptionHandlerListener {
    @Override
    public ApiExceptionHandlerListenerResult shouldHandleException(Throwable ex) {
        if (ex instanceof MyFrameworkException myEx) {
            // The exception is a MyFrameworkException, so this listener should handle it.
            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", singletonList(myEx.foo())),
                Pair.of("bar-info", singletonList(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.

Integration Modules

General-Purpose Modules

  • backstopper-core - The core library providing the majority of the Backstopper functionality.
  • backstopper-reusable-tests-junit5 - There are some rules around defining your project's set of API errors and conventions around integration with Java's JSR 303 Bean Validation system that must be followed for Backstopper to function at its best. This library contains reusable tests that are intended to be included in a Backstopper-enabled project's unit test suite to guarantee that these rules are adhered to and fail the build with descriptive unit test errors when those rules are violated. These are technically optional but highly recommended - integration is generally quick and easy.
  • backstopper-custom-validators - This library contains JSR 303 Bean Validation annotations that have proven to be useful and reusable. These are entirely optional. They are also largely dependency free so this library is usable in non-Backstopper projects that utilize JSR 303 validations.
  • backstopper-jackson - Contains a few utilities that help integrate Backstopper and Jackson for serializing error contracts to JSON. Optional.
  • backstopper-servlet-api - Intermediate library intended to ease integration with Servlet-based frameworks. If you're building a Backstopper integration library for a Servlet-based framework that we don't already have support for then you'll want to use this.
  • backstopper-spring-web - Intermediate library intended to ease integration with Spring-based frameworks. If you're building a Spring integration library for a Spring-based framework that we don't already have support for then you'll want to use this.
  • nike-internal-util - A small utilities library that provides some reusable helper methods and classes. It is "internal" in the sense that it is not intended to be directly pulled in and used by non-Nike projects. That said you can use it if you want to, just be aware that some liberties might be taken regarding version numbers, backwards compatibility, etc over time when compared with libraries specifically intended for public consumption.

Framework-Specific Modules

  • backstopper-spring-web-mvc - Base Integration library for the Spring Web MVC (Servlet) framework. If you want to integrate Backstopper into a project running in Spring Web MVC then start here. Works for Spring 6+, and used as a foundation for Backstopper support in Spring Boot 3 MVC (see below for links to Spring Boot specific integration modules). There is a Spring Web MVC sample project complete with integration tests you can use as an example.
  • backstopper-spring-web-flux - Integration library for the Spring WebFlux (Netty) framework. If you want to integrate Backstopper into a project running in Spring WebFlux then start here. There is a Spring Boot 3 WebFlux sample project complete with integration tests you can use as an example.
  • backstopper-spring-boot3-webmvc - Integration library for the Spring Boot 3 framework if and only if you're using the Spring Web MVC Servlet runtime (if you're running a Spring Boot 3 + WebFlux Netty application, then you do not want this library and should use backstopper-spring-web-flux instead). If you want to integrate Backstopper into a project running in Spring Boot 3 + Web MVC then start here. There is a Spring Boot 3 Web MVC sample project complete with integration tests you can use as an example.

NOTE: Backstopper 1.x releases contain support for the javax ecosystem, JAX-RS 2, Jersey 1 and 2, Spring 4 and 5, and Springboot 1 and 2 - see here if you need support for these older frameworks.

We have not created support for jakarta based JAX-RS or Jersey frameworks in Backstopper 2.x due to lack of interest, but it could be done.

Framework Integration Sample Applications

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.

User Guide

For further details please consult the User Guide.

License

Backstopper is released under the Apache License, Version 2.0

About

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

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Java 100.0%