github.com/cosmos/cosmos-sdk@v0.50.10/docs/architecture/adr-022-custom-panic-handling.md (about)

     1  # ADR 022: Custom BaseApp panic handling
     2  
     3  ## Changelog
     4  
     5  * 2020 Apr 24: Initial Draft
     6  * 2021 Sep 14: Superseded by ADR-045
     7  
     8  ## Status
     9  
    10  SUPERSEDED by ADR-045
    11  
    12  ## Context
    13  
    14  The current implementation of BaseApp does not allow developers to write custom error handlers during panic recovery
    15  [runTx()](https://github.com/cosmos/cosmos-sdk/blob/bad4ca75f58b182f600396ca350ad844c18fc80b/baseapp/baseapp.go#L539)
    16  method. We think that this method can be more flexible and can give Cosmos SDK users more options for customizations without
    17  the need to rewrite whole BaseApp. Also there's one special case for `sdk.ErrorOutOfGas` error handling, that case
    18  might be handled in a "standard" way (middleware) alongside the others.
    19  
    20  We propose middleware-solution, which could help developers implement the following cases:
    21  
    22  * add external logging (let's say sending reports to external services like [Sentry](https://sentry.io));
    23  * call panic for specific error cases;
    24  
    25  It will also make `OutOfGas` case and `default` case one of the middlewares.
    26  `Default` case wraps recovery object to an error and logs it ([example middleware implementation](#Recovery-middleware)).
    27  
    28  Our project has a sidecar service running alongside the blockchain node (smart contracts virtual machine). It is
    29  essential that node <-> sidecar connectivity stays stable for TXs processing. So when the communication breaks we need
    30  to crash the node and reboot it once the problem is solved. That behaviour makes node's state machine execution
    31  deterministic. As all keeper panics are caught by runTx's `defer()` handler, we have to adjust the BaseApp code
    32  in order to customize it.
    33  
    34  ## Decision
    35  
    36  ### Design
    37  
    38  #### Overview
    39  
    40  Instead of hardcoding custom error handling into BaseApp we suggest using set of middlewares which can be customized
    41  externally and will allow developers use as many custom error handlers as they want. Implementation with tests
    42  can be found [here](https://github.com/cosmos/cosmos-sdk/pull/6053).
    43  
    44  #### Implementation details
    45  
    46  ##### Recovery handler
    47  
    48  New `RecoveryHandler` type added. `recoveryObj` input argument is an object returned by the standard Go function
    49  `recover()` from the `builtin` package.
    50  
    51  ```go
    52  type RecoveryHandler func(recoveryObj interface{}) error
    53  ```
    54  
    55  Handler should type assert (or other methods) an object to define if object should be handled.
    56  `nil` should be returned if input object can't be handled by that `RecoveryHandler` (not a handler's target type).
    57  Not `nil` error should be returned if input object was handled and middleware chain execution should be stopped.
    58  
    59  An example:
    60  
    61  ```go
    62  func exampleErrHandler(recoveryObj interface{}) error {
    63      err, ok := recoveryObj.(error)
    64      if !ok { return nil }
    65  
    66      if someSpecificError.Is(err) {
    67          panic(customPanicMsg)
    68      } else {
    69          return nil
    70      }
    71  }
    72  ```
    73  
    74  This example breaks the application execution, but it also might enrich the error's context like the `OutOfGas` handler.
    75  
    76  ##### Recovery middleware
    77  
    78  We also add a middleware type (decorator). That function type wraps `RecoveryHandler` and returns the next middleware in
    79  execution chain and handler's `error`. Type is used to separate actual `recovery()` object handling from middleware
    80  chain processing.
    81  
    82  ```go
    83  type recoveryMiddleware func(recoveryObj interface{}) (recoveryMiddleware, error)
    84  
    85  func newRecoveryMiddleware(handler RecoveryHandler, next recoveryMiddleware) recoveryMiddleware {
    86      return func(recoveryObj interface{}) (recoveryMiddleware, error) {
    87          if err := handler(recoveryObj); err != nil {
    88              return nil, err
    89          }
    90          return next, nil
    91      }
    92  }
    93  ```
    94  
    95  Function receives a `recoveryObj` object and returns:
    96  
    97  * (next `recoveryMiddleware`, `nil`) if object wasn't handled (not a target type) by `RecoveryHandler`;
    98  * (`nil`, not nil `error`) if input object was handled and other middlewares in the chain should not be executed;
    99  * (`nil`, `nil`) in case of invalid behavior. Panic recovery might not have been properly handled;
   100  this can be avoided by always using a `default` as a rightmost middleware in the chain (always returns an `error`');
   101  
   102  `OutOfGas` middleware example:
   103  
   104  ```go
   105  func newOutOfGasRecoveryMiddleware(gasWanted uint64, ctx sdk.Context, next recoveryMiddleware) recoveryMiddleware {
   106      handler := func(recoveryObj interface{}) error {
   107          err, ok := recoveryObj.(sdk.ErrorOutOfGas)
   108          if !ok { return nil }
   109  
   110          return errorsmod.Wrap(
   111              sdkerrors.ErrOutOfGas, fmt.Sprintf(
   112                  "out of gas in location: %v; gasWanted: %d, gasUsed: %d", err.Descriptor, gasWanted, ctx.GasMeter().GasConsumed(),
   113              ),
   114          )
   115      }
   116  
   117      return newRecoveryMiddleware(handler, next)
   118  }
   119  ```
   120  
   121  `Default` middleware example:
   122  
   123  ```go
   124  func newDefaultRecoveryMiddleware() recoveryMiddleware {
   125      handler := func(recoveryObj interface{}) error {
   126          return errorsmod.Wrap(
   127              sdkerrors.ErrPanic, fmt.Sprintf("recovered: %v\nstack:\n%v", recoveryObj, string(debug.Stack())),
   128          )
   129      }
   130  
   131      return newRecoveryMiddleware(handler, nil)
   132  }
   133  ```
   134  
   135  ##### Recovery processing
   136  
   137  Basic chain of middlewares processing would look like:
   138  
   139  ```go
   140  func processRecovery(recoveryObj interface{}, middleware recoveryMiddleware) error {
   141  	if middleware == nil { return nil }
   142  
   143  	next, err := middleware(recoveryObj)
   144  	if err != nil { return err }
   145  	if next == nil { return nil }
   146  
   147  	return processRecovery(recoveryObj, next)
   148  }
   149  ```
   150  
   151  That way we can create a middleware chain which is executed from left to right, the rightmost middleware is a
   152  `default` handler which must return an `error`.
   153  
   154  ##### BaseApp changes
   155  
   156  The `default` middleware chain must exist in a `BaseApp` object. `Baseapp` modifications:
   157  
   158  ```go
   159  type BaseApp struct {
   160      // ...
   161      runTxRecoveryMiddleware recoveryMiddleware
   162  }
   163  
   164  func NewBaseApp(...) {
   165      // ...
   166      app.runTxRecoveryMiddleware = newDefaultRecoveryMiddleware()
   167  }
   168  
   169  func (app *BaseApp) runTx(...) {
   170      // ...
   171      defer func() {
   172          if r := recover(); r != nil {
   173              recoveryMW := newOutOfGasRecoveryMiddleware(gasWanted, ctx, app.runTxRecoveryMiddleware)
   174              err, result = processRecovery(r, recoveryMW), nil
   175          }
   176  
   177          gInfo = sdk.GasInfo{GasWanted: gasWanted, GasUsed: ctx.GasMeter().GasConsumed()}
   178      }()
   179      // ...
   180  }
   181  ```
   182  
   183  Developers can add their custom `RecoveryHandler`s by providing `AddRunTxRecoveryHandler` as a BaseApp option parameter to the `NewBaseapp` constructor:
   184  
   185  ```go
   186  func (app *BaseApp) AddRunTxRecoveryHandler(handlers ...RecoveryHandler) {
   187      for _, h := range handlers {
   188          app.runTxRecoveryMiddleware = newRecoveryMiddleware(h, app.runTxRecoveryMiddleware)
   189      }
   190  }
   191  ```
   192  
   193  This method would prepend handlers to an existing chain.
   194  
   195  ## Consequences
   196  
   197  ### Positive
   198  
   199  * Developers of Cosmos SDK based projects can add custom panic handlers to:
   200      * add error context for custom panic sources (panic inside of custom keepers);
   201      * emit `panic()`: passthrough recovery object to the Tendermint core;
   202      * other necessary handling;
   203  * Developers can use standard Cosmos SDK `BaseApp` implementation, rather that rewriting it in their projects;
   204  * Proposed solution doesn't break the current "standard" `runTx()` flow;
   205  
   206  ### Negative
   207  
   208  * Introduces changes to the execution model design.
   209  
   210  ### Neutral
   211  
   212  * `OutOfGas` error handler becomes one of the middlewares;
   213  * Default panic handler becomes one of the middlewares;
   214  
   215  ## References
   216  
   217  * [PR-6053 with proposed solution](https://github.com/cosmos/cosmos-sdk/pull/6053)
   218  * [Similar solution. ADR-010 Modular AnteHandler](https://github.com/cosmos/cosmos-sdk/blob/main/docs/architecture/adr-010-modular-antehandler.md)