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)