Skip to content

Commit

Permalink
Merge pull request #5545 from IntersectMBO/newhoggy/structured-errors
Browse files Browse the repository at this point in the history
Render errors as JSON in http response
  • Loading branch information
newhoggy committed Jan 14, 2024
2 parents 630d9f2 + 9d77edc commit 54252c5
Show file tree
Hide file tree
Showing 10 changed files with 124 additions and 65 deletions.
3 changes: 1 addition & 2 deletions cardano-submit-api/cardano-submit-api.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ library
, cardano-binary
, cardano-cli ^>= 8.18.0.0
, cardano-crypto-class ^>= 2.1.2
, formatting
, http-media
, iohk-monitoring
, mtl
Expand All @@ -68,8 +67,8 @@ library
other-modules: Cardano.TxSubmit.CLI.Parsers
, Cardano.TxSubmit.CLI.Types
, Cardano.TxSubmit.Config
, Cardano.TxSubmit.ErrorRender
, Cardano.TxSubmit.Metrics
, Cardano.TxSubmit.Orphans
, Cardano.TxSubmit.Rest.Parsers
, Cardano.TxSubmit.Rest.Types
, Cardano.TxSubmit.Rest.Web
Expand Down
21 changes: 0 additions & 21 deletions cardano-submit-api/src/Cardano/TxSubmit/ErrorRender.hs

This file was deleted.

20 changes: 20 additions & 0 deletions cardano-submit-api/src/Cardano/TxSubmit/Orphans.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE StandaloneDeriving #-}

{-# OPTIONS_GHC -Wno-orphans #-}

module Cardano.TxSubmit.Orphans
(
) where

import Cardano.Api
import Cardano.Binary (DecoderError)
import Data.Aeson (ToJSON (..))
import qualified Data.Aeson as Aeson
import Ouroboros.Consensus.Cardano.Block

instance ToJSON DecoderError where
toJSON = Aeson.String . textShow

deriving anyclass instance ToJSON EraMismatch
80 changes: 54 additions & 26 deletions cardano-submit-api/src/Cardano/TxSubmit/Types.hs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeOperators #-}

module Cardano.TxSubmit.Types
Expand All @@ -12,36 +16,38 @@ module Cardano.TxSubmit.Types
, EnvSocketError(..)
, TxCmdError(..)
, RawCborDecodeError(..)
, renderTxSubmitWebApiError
, renderTxCmdError
) where

import Cardano.Api (Error (..), TxId, textShow)
import Cardano.Api (Error (..), TxId, TxValidationErrorInCardanoMode (..), textShow)
import Cardano.Api.Pretty
import Cardano.Binary (DecoderError)
import Data.Aeson (ToJSON (..), Value (..))
import Cardano.TxSubmit.Orphans ()
import Data.Aeson (ToJSON (..), Value (..), (.=))
import qualified Data.Aeson as Aeson
import Data.ByteString.Char8 (ByteString)
import Data.Text (Text)
import Formatting (build, sformat)
import GHC.Generics (Generic)
import Network.HTTP.Media ((//))
import Ouroboros.Consensus.Cardano.Block (EraMismatch (..))
import Servant (Accept (..), JSON, MimeRender (..), MimeUnrender (..), PostAccepted,
ReqBody, (:>))
import Servant.API.Generic (ToServantApi, (:-))

import qualified Data.ByteString.Lazy.Char8 as LBS
import qualified Data.Text as T

newtype TxSubmitPort = TxSubmitPort Int

-- | The errors that the raw CBOR transaction parsing\/decoding functions can return.
--
newtype RawCborDecodeError = RawCborDecodeError [DecoderError]
deriving (Eq, Show)
deriving (Eq, Generic, Show)

instance Error RawCborDecodeError where
prettyError (RawCborDecodeError decodeErrors) = "RawCborDecodeError decode error: " <> pshow (fmap pshow decodeErrors)

deriving anyclass instance ToJSON RawCborDecodeError

-- | An error that can occur in the transaction submission web API.
data TxSubmitWebApiError
= TxSubmitDecodeHex
Expand All @@ -50,34 +56,56 @@ data TxSubmitWebApiError
| TxSubmitBadTx !Text
| TxSubmitFail TxCmdError

newtype EnvSocketError = CliEnvVarLookup Text deriving (Eq, Show)
deriving instance Generic TxSubmitWebApiError

instance ToJSON TxSubmitWebApiError where
toJSON = \case
TxSubmitDecodeHex -> Aeson.object
[ "tag" .= String "TxSubmitDecodeHex"
]
TxSubmitEmpty -> Aeson.object
[ "tag" .= String "TxSubmitEmpty"
]
TxSubmitDecodeFail err -> Aeson.object
[ "tag" .= String "TxSubmitDecodeFail"
, "contents" .= toJSON err
]
TxSubmitBadTx err -> Aeson.object
[ "tag" .= String "TxSubmitBadTx"
, "contents" .= toJSON err
]
TxSubmitFail err -> Aeson.object
[ "tag" .= String "TxSubmitFail"
, "contents" .= toJSON err
]

newtype EnvSocketError = CliEnvVarLookup Text
deriving (Eq, Generic, Show)

instance ToJSON EnvSocketError where
toJSON (CliEnvVarLookup msg) = Aeson.object
[ "message" .= String msg
]

data TxCmdError
= TxCmdSocketEnvError EnvSocketError
| TxCmdTxReadError !RawCborDecodeError
| TxCmdTxSubmitError !Text
| TxCmdTxSubmitErrorEraMismatch !EraMismatch
| TxCmdTxSubmitValidationError !TxValidationErrorInCardanoMode

instance ToJSON TxSubmitWebApiError where
toJSON = convertJson
deriving instance Generic TxCmdError

convertJson :: TxSubmitWebApiError -> Value
convertJson = String . renderTxSubmitWebApiError
deriving anyclass instance ToJSON TxCmdError

renderTxCmdError :: TxCmdError -> Text
renderTxCmdError (TxCmdSocketEnvError socketError) = "socket env error " <> textShow socketError
renderTxCmdError (TxCmdTxReadError envelopeError) = "transaction read error " <> textShow envelopeError
renderTxCmdError (TxCmdTxSubmitError msg) = "transaction submit error " <> msg
renderTxCmdError (TxCmdTxSubmitErrorEraMismatch eraMismatch) = "transaction submit era mismatch" <> textShow eraMismatch

renderTxSubmitWebApiError :: TxSubmitWebApiError -> Text
renderTxSubmitWebApiError st =
case st of
TxSubmitDecodeHex -> "Provided data was hex encoded and this webapi expects raw binary"
TxSubmitEmpty -> "Provided transaction has zero length"
TxSubmitDecodeFail err -> sformat build err
TxSubmitBadTx tt -> mconcat ["Transactions of type '", tt, "' not supported"]
TxSubmitFail err -> renderTxCmdError err
renderTxCmdError = \case
TxCmdSocketEnvError socketError ->
"socket env error " <> textShow socketError
TxCmdTxReadError envelopeError ->
"transaction read error " <> textShow envelopeError
TxCmdTxSubmitValidationError e ->
case e of
TxValidationErrorInCardanoMode err -> "transaction submit error " <> T.pack (show err)
TxValidationEraMismatch eraMismatch -> "transaction submit era mismatch" <> textShow eraMismatch

-- | Servant API which provides access to tx submission webapi
type TxSubmitApi = "api" :> ToServantApi TxSubmitApiRecord
Expand Down
23 changes: 10 additions & 13 deletions cardano-submit-api/src/Cardano/TxSubmit/Web.hs
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ import qualified Cardano.Crypto.Hash.Class as Crypto
import Cardano.TxSubmit.Metrics (TxSubmitMetrics (..))
import Cardano.TxSubmit.Rest.Types (WebserverConfig (..), toWarpSettings)
import qualified Cardano.TxSubmit.Rest.Web as Web
import Cardano.TxSubmit.Types (EnvSocketError (..), RawCborDecodeError (..),
TxCmdError (TxCmdTxReadError, TxCmdTxSubmitError, TxCmdTxSubmitErrorEraMismatch),
TxSubmitApi, TxSubmitApiRecord (..), TxSubmitWebApiError (TxSubmitFail),
renderTxCmdError)

import Cardano.TxSubmit.Util (logException)
import Ouroboros.Consensus.Cardano.Block (EraMismatch (..))
import qualified Ouroboros.Network.Protocol.LocalTxSubmission.Client as Net.Tx

import Cardano.TxSubmit.Types (EnvSocketError (..), RawCborDecodeError (..),
TxCmdError (..), TxSubmitApi, TxSubmitApiRecord (..),
TxSubmitWebApiError (TxSubmitFail), renderTxCmdError)
import Control.Applicative (Applicative (pure), (<$>))
import Control.Monad (Functor (fmap), Monad (return), (=<<))
import Control.Monad.Except (ExceptT, MonadError (throwError), runExceptT)
Expand All @@ -59,17 +59,16 @@ import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Text.IO as T
import qualified Servant
import Servant (Application, Handler, ServerError (..), err400, throwError)
import Servant.API.Generic (toServant)
import Servant.Server.Generic (AsServerT)
import System.Environment (lookupEnv)
import qualified System.IO as IO
import System.IO (IO)
import qualified System.Metrics.Prometheus.Metric.Gauge as Gauge
import Text.Show (Show (show))

import qualified Servant
import Servant (Application, Handler, ServerError (..), err400, throwError)
import Servant.API.Generic (toServant)
import Servant.Server.Generic (AsServerT)

runTxSubmitServer
:: Trace IO Text
-> TxSubmitMetrics
Expand Down Expand Up @@ -147,10 +146,8 @@ txSubmitPost trace metrics p@(CardanoModeParams cModeParams) networkId socketPat
Net.Tx.SubmitSuccess -> do
liftIO $ T.putStrLn "Transaction successfully submitted."
return $ getTxId (getTxBody tx)
Net.Tx.SubmitFail reason ->
case reason of
TxValidationErrorInCardanoMode err -> left . TxCmdTxSubmitError . T.pack $ show err
TxValidationEraMismatch mismatchErr -> left $ TxCmdTxSubmitErrorEraMismatch mismatchErr
Net.Tx.SubmitFail e ->
left $ TxCmdTxSubmitValidationError e
where
handle :: ExceptT TxCmdError IO TxId -> Handler TxId
handle f = do
Expand Down
1 change: 1 addition & 0 deletions cardano-testnet/cardano-testnet.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ test-suite cardano-testnet-test
type: exitcode-stdio-1.0

build-depends: aeson
, aeson-pretty
, async
, base16-bytestring
, bytestring
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ import qualified Testnet.Property.Utils as H
import Testnet.Runtime

import qualified Cardano.Api.Ledger as L
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Encode.Pretty as Aeson
import qualified Data.Aeson.Lens as Aeson
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base16 as Base16
Expand Down Expand Up @@ -93,6 +95,8 @@ hprop_transaction = H.integrationRetryWorkspace 0 "submit-api-babbage-transactio
txbodySignedFp <- H.note $ work </> "tx.body.signed"
txbodySignedBinFp <- H.note $ work </> "tx.body.signed.bin"
txFailedResponseFp <- H.note $ work </> "tx.failed.response"
txFailedResponseYamlFp <- H.note $ work </> "tx.failed.response.json"
txFailedResponseYamlGoldenFp <- H.note "test/cardano-testnet-test/files/golden/tx.failed.response.json.golden"

void $ execCli' execConfig
[ "babbage", "query", "utxo"
Expand Down Expand Up @@ -190,8 +194,11 @@ hprop_transaction = H.integrationRetryWorkspace 0 "submit-api-babbage-transactio

H.evalIO $ LBS.writeFile txFailedResponseFp $ redactHashLbs $ getResponseBody response

H.diffFileVsGoldenFile txFailedResponseFp "test/cardano-testnet-test/files/golden/tx.failed.response.golden"
v <- H.leftFailM $ H.evalIO $ Aeson.eitherDecodeFileStrict @Aeson.Value txFailedResponseFp

H.evalIO $ LBS.writeFile txFailedResponseYamlFp $ Aeson.encodePretty v

H.diffFileVsGoldenFile txFailedResponseYamlFp txFailedResponseYamlGoldenFp

redactHashLbs :: LBS.ByteString -> LBS.ByteString
redactHashLbs = id
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"contents": {
"contents": {
"contents": {
"era": "ShelleyBasedEraBabbage",
"error": [
{
"contents": {
"contents": "AlonzoInBabbageUtxoPredFailure (ValueNotConservedUTxO (MaryValue (Coin 0) (MultiAsset (fromList []))) (MaryValue (Coin 300000000000) (MultiAsset (fromList []))))",
"tag": "UtxoFailure"
},
"tag": "UtxowFailure"
},
{
"contents": {
"contents": "AlonzoInBabbageUtxoPredFailure (BadInputsUTxO (fromList [TxIn (TxId {unTxId = SafeHash \"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"}) (TxIx 0)]))",
"tag": "UtxoFailure"
},
"tag": "UtxowFailure"
}
],
"kind": "ShelleyTxValidationError"
},
"tag": "TxValidationErrorInCardanoMode"
},
"tag": "TxCmdTxSubmitValidationError"
},
"tag": "TxSubmitFail"
}
2 changes: 1 addition & 1 deletion nix/haskell.nix
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ let
"cardano-testnet/test/cardano-testnet-golden/files/golden/mary_node_default_config.json"
"cardano-testnet/test/cardano-testnet-golden/files/golden/shelley_node_default_config.json"
"cardano-testnet/test/cardano-testnet-golden/files/golden/shelley_node_default_config.json"
"cardano-testnet/test/cardano-testnet-test/files/golden/tx.failed.response.golden"
"cardano-testnet/test/cardano-testnet-test/files/golden/tx.failed.response.json.golden"
"cardano-testnet/files/data/alonzo/genesis.alonzo.spec.json"
"cardano-testnet/files/data/conway/genesis.conway.spec.json"
];
Expand Down

0 comments on commit 54252c5

Please sign in to comment.