Skip to content

Commit 54252c5

Browse files
authored
Merge pull request #5545 from IntersectMBO/newhoggy/structured-errors
Render errors as JSON in http response
2 parents 630d9f2 + 9d77edc commit 54252c5

10 files changed

Lines changed: 124 additions & 65 deletions

File tree

cardano-submit-api/cardano-submit-api.cabal

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ library
4343
, cardano-binary
4444
, cardano-cli ^>= 8.18.0.0
4545
, cardano-crypto-class ^>= 2.1.2
46-
, formatting
4746
, http-media
4847
, iohk-monitoring
4948
, mtl
@@ -68,8 +67,8 @@ library
6867
other-modules: Cardano.TxSubmit.CLI.Parsers
6968
, Cardano.TxSubmit.CLI.Types
7069
, Cardano.TxSubmit.Config
71-
, Cardano.TxSubmit.ErrorRender
7270
, Cardano.TxSubmit.Metrics
71+
, Cardano.TxSubmit.Orphans
7372
, Cardano.TxSubmit.Rest.Parsers
7473
, Cardano.TxSubmit.Rest.Types
7574
, Cardano.TxSubmit.Rest.Web

cardano-submit-api/src/Cardano/TxSubmit/ErrorRender.hs

Lines changed: 0 additions & 21 deletions
This file was deleted.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{-# LANGUAGE DeriveAnyClass #-}
2+
{-# LANGUAGE DerivingStrategies #-}
3+
{-# LANGUAGE StandaloneDeriving #-}
4+
5+
{-# OPTIONS_GHC -Wno-orphans #-}
6+
7+
module Cardano.TxSubmit.Orphans
8+
(
9+
) where
10+
11+
import Cardano.Api
12+
import Cardano.Binary (DecoderError)
13+
import Data.Aeson (ToJSON (..))
14+
import qualified Data.Aeson as Aeson
15+
import Ouroboros.Consensus.Cardano.Block
16+
17+
instance ToJSON DecoderError where
18+
toJSON = Aeson.String . textShow
19+
20+
deriving anyclass instance ToJSON EraMismatch

cardano-submit-api/src/Cardano/TxSubmit/Types.hs

Lines changed: 54 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
{-# LANGUAGE DataKinds #-}
2+
{-# LANGUAGE DeriveAnyClass #-}
23
{-# LANGUAGE DeriveGeneric #-}
4+
{-# LANGUAGE DerivingStrategies #-}
5+
{-# LANGUAGE LambdaCase #-}
36
{-# LANGUAGE MultiParamTypeClasses #-}
47
{-# LANGUAGE OverloadedStrings #-}
8+
{-# LANGUAGE StandaloneDeriving #-}
59
{-# LANGUAGE TypeOperators #-}
610

711
module Cardano.TxSubmit.Types
@@ -12,36 +16,38 @@ module Cardano.TxSubmit.Types
1216
, EnvSocketError(..)
1317
, TxCmdError(..)
1418
, RawCborDecodeError(..)
15-
, renderTxSubmitWebApiError
1619
, renderTxCmdError
1720
) where
1821

19-
import Cardano.Api (Error (..), TxId, textShow)
22+
import Cardano.Api (Error (..), TxId, TxValidationErrorInCardanoMode (..), textShow)
2023
import Cardano.Api.Pretty
2124
import Cardano.Binary (DecoderError)
22-
import Data.Aeson (ToJSON (..), Value (..))
25+
import Cardano.TxSubmit.Orphans ()
26+
import Data.Aeson (ToJSON (..), Value (..), (.=))
27+
import qualified Data.Aeson as Aeson
2328
import Data.ByteString.Char8 (ByteString)
2429
import Data.Text (Text)
25-
import Formatting (build, sformat)
2630
import GHC.Generics (Generic)
2731
import Network.HTTP.Media ((//))
28-
import Ouroboros.Consensus.Cardano.Block (EraMismatch (..))
2932
import Servant (Accept (..), JSON, MimeRender (..), MimeUnrender (..), PostAccepted,
3033
ReqBody, (:>))
3134
import Servant.API.Generic (ToServantApi, (:-))
3235

3336
import qualified Data.ByteString.Lazy.Char8 as LBS
37+
import qualified Data.Text as T
3438

3539
newtype TxSubmitPort = TxSubmitPort Int
3640

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

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

49+
deriving anyclass instance ToJSON RawCborDecodeError
50+
4551
-- | An error that can occur in the transaction submission web API.
4652
data TxSubmitWebApiError
4753
= TxSubmitDecodeHex
@@ -50,34 +56,56 @@ data TxSubmitWebApiError
5056
| TxSubmitBadTx !Text
5157
| TxSubmitFail TxCmdError
5258

53-
newtype EnvSocketError = CliEnvVarLookup Text deriving (Eq, Show)
59+
deriving instance Generic TxSubmitWebApiError
60+
61+
instance ToJSON TxSubmitWebApiError where
62+
toJSON = \case
63+
TxSubmitDecodeHex -> Aeson.object
64+
[ "tag" .= String "TxSubmitDecodeHex"
65+
]
66+
TxSubmitEmpty -> Aeson.object
67+
[ "tag" .= String "TxSubmitEmpty"
68+
]
69+
TxSubmitDecodeFail err -> Aeson.object
70+
[ "tag" .= String "TxSubmitDecodeFail"
71+
, "contents" .= toJSON err
72+
]
73+
TxSubmitBadTx err -> Aeson.object
74+
[ "tag" .= String "TxSubmitBadTx"
75+
, "contents" .= toJSON err
76+
]
77+
TxSubmitFail err -> Aeson.object
78+
[ "tag" .= String "TxSubmitFail"
79+
, "contents" .= toJSON err
80+
]
81+
82+
newtype EnvSocketError = CliEnvVarLookup Text
83+
deriving (Eq, Generic, Show)
84+
85+
instance ToJSON EnvSocketError where
86+
toJSON (CliEnvVarLookup msg) = Aeson.object
87+
[ "message" .= String msg
88+
]
5489

5590
data TxCmdError
5691
= TxCmdSocketEnvError EnvSocketError
5792
| TxCmdTxReadError !RawCborDecodeError
58-
| TxCmdTxSubmitError !Text
59-
| TxCmdTxSubmitErrorEraMismatch !EraMismatch
93+
| TxCmdTxSubmitValidationError !TxValidationErrorInCardanoMode
6094

61-
instance ToJSON TxSubmitWebApiError where
62-
toJSON = convertJson
95+
deriving instance Generic TxCmdError
6396

64-
convertJson :: TxSubmitWebApiError -> Value
65-
convertJson = String . renderTxSubmitWebApiError
97+
deriving anyclass instance ToJSON TxCmdError
6698

6799
renderTxCmdError :: TxCmdError -> Text
68-
renderTxCmdError (TxCmdSocketEnvError socketError) = "socket env error " <> textShow socketError
69-
renderTxCmdError (TxCmdTxReadError envelopeError) = "transaction read error " <> textShow envelopeError
70-
renderTxCmdError (TxCmdTxSubmitError msg) = "transaction submit error " <> msg
71-
renderTxCmdError (TxCmdTxSubmitErrorEraMismatch eraMismatch) = "transaction submit era mismatch" <> textShow eraMismatch
72-
73-
renderTxSubmitWebApiError :: TxSubmitWebApiError -> Text
74-
renderTxSubmitWebApiError st =
75-
case st of
76-
TxSubmitDecodeHex -> "Provided data was hex encoded and this webapi expects raw binary"
77-
TxSubmitEmpty -> "Provided transaction has zero length"
78-
TxSubmitDecodeFail err -> sformat build err
79-
TxSubmitBadTx tt -> mconcat ["Transactions of type '", tt, "' not supported"]
80-
TxSubmitFail err -> renderTxCmdError err
100+
renderTxCmdError = \case
101+
TxCmdSocketEnvError socketError ->
102+
"socket env error " <> textShow socketError
103+
TxCmdTxReadError envelopeError ->
104+
"transaction read error " <> textShow envelopeError
105+
TxCmdTxSubmitValidationError e ->
106+
case e of
107+
TxValidationErrorInCardanoMode err -> "transaction submit error " <> T.pack (show err)
108+
TxValidationEraMismatch eraMismatch -> "transaction submit era mismatch" <> textShow eraMismatch
81109

82110
-- | Servant API which provides access to tx submission webapi
83111
type TxSubmitApi = "api" :> ToServantApi TxSubmitApiRecord

cardano-submit-api/src/Cardano/TxSubmit/Web.hs

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,14 @@ import qualified Cardano.Crypto.Hash.Class as Crypto
2626
import Cardano.TxSubmit.Metrics (TxSubmitMetrics (..))
2727
import Cardano.TxSubmit.Rest.Types (WebserverConfig (..), toWarpSettings)
2828
import qualified Cardano.TxSubmit.Rest.Web as Web
29-
import Cardano.TxSubmit.Types (EnvSocketError (..), RawCborDecodeError (..),
30-
TxCmdError (TxCmdTxReadError, TxCmdTxSubmitError, TxCmdTxSubmitErrorEraMismatch),
31-
TxSubmitApi, TxSubmitApiRecord (..), TxSubmitWebApiError (TxSubmitFail),
32-
renderTxCmdError)
29+
3330
import Cardano.TxSubmit.Util (logException)
3431
import Ouroboros.Consensus.Cardano.Block (EraMismatch (..))
3532
import qualified Ouroboros.Network.Protocol.LocalTxSubmission.Client as Net.Tx
3633

34+
import Cardano.TxSubmit.Types (EnvSocketError (..), RawCborDecodeError (..),
35+
TxCmdError (..), TxSubmitApi, TxSubmitApiRecord (..),
36+
TxSubmitWebApiError (TxSubmitFail), renderTxCmdError)
3737
import Control.Applicative (Applicative (pure), (<$>))
3838
import Control.Monad (Functor (fmap), Monad (return), (=<<))
3939
import Control.Monad.Except (ExceptT, MonadError (throwError), runExceptT)
@@ -59,17 +59,16 @@ import Data.Text (Text)
5959
import qualified Data.Text as T
6060
import qualified Data.Text.Encoding as T
6161
import qualified Data.Text.IO as T
62+
import qualified Servant
63+
import Servant (Application, Handler, ServerError (..), err400, throwError)
64+
import Servant.API.Generic (toServant)
65+
import Servant.Server.Generic (AsServerT)
6266
import System.Environment (lookupEnv)
6367
import qualified System.IO as IO
6468
import System.IO (IO)
6569
import qualified System.Metrics.Prometheus.Metric.Gauge as Gauge
6670
import Text.Show (Show (show))
6771

68-
import qualified Servant
69-
import Servant (Application, Handler, ServerError (..), err400, throwError)
70-
import Servant.API.Generic (toServant)
71-
import Servant.Server.Generic (AsServerT)
72-
7372
runTxSubmitServer
7473
:: Trace IO Text
7574
-> TxSubmitMetrics
@@ -147,10 +146,8 @@ txSubmitPost trace metrics p@(CardanoModeParams cModeParams) networkId socketPat
147146
Net.Tx.SubmitSuccess -> do
148147
liftIO $ T.putStrLn "Transaction successfully submitted."
149148
return $ getTxId (getTxBody tx)
150-
Net.Tx.SubmitFail reason ->
151-
case reason of
152-
TxValidationErrorInCardanoMode err -> left . TxCmdTxSubmitError . T.pack $ show err
153-
TxValidationEraMismatch mismatchErr -> left $ TxCmdTxSubmitErrorEraMismatch mismatchErr
149+
Net.Tx.SubmitFail e ->
150+
left $ TxCmdTxSubmitValidationError e
154151
where
155152
handle :: ExceptT TxCmdError IO TxId -> Handler TxId
156153
handle f = do

cardano-testnet/cardano-testnet.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ test-suite cardano-testnet-test
186186
type: exitcode-stdio-1.0
187187

188188
build-depends: aeson
189+
, aeson-pretty
189190
, async
190191
, base16-bytestring
191192
, bytestring

cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/SubmitApi/Babbage/Transaction.hs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ import qualified Testnet.Property.Utils as H
4343
import Testnet.Runtime
4444

4545
import qualified Cardano.Api.Ledger as L
46+
import qualified Data.Aeson as Aeson
47+
import qualified Data.Aeson.Encode.Pretty as Aeson
4648
import qualified Data.Aeson.Lens as Aeson
4749
import qualified Data.ByteString as BS
4850
import qualified Data.ByteString.Base16 as Base16
@@ -93,6 +95,8 @@ hprop_transaction = H.integrationRetryWorkspace 0 "submit-api-babbage-transactio
9395
txbodySignedFp <- H.note $ work </> "tx.body.signed"
9496
txbodySignedBinFp <- H.note $ work </> "tx.body.signed.bin"
9597
txFailedResponseFp <- H.note $ work </> "tx.failed.response"
98+
txFailedResponseYamlFp <- H.note $ work </> "tx.failed.response.json"
99+
txFailedResponseYamlGoldenFp <- H.note "test/cardano-testnet-test/files/golden/tx.failed.response.json.golden"
96100

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

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

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

199+
H.evalIO $ LBS.writeFile txFailedResponseYamlFp $ Aeson.encodePretty v
200+
201+
H.diffFileVsGoldenFile txFailedResponseYamlFp txFailedResponseYamlGoldenFp
195202

196203
redactHashLbs :: LBS.ByteString -> LBS.ByteString
197204
redactHashLbs = id

cardano-testnet/test/cardano-testnet-test/files/golden/tx.failed.response.golden

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"contents": {
3+
"contents": {
4+
"contents": {
5+
"era": "ShelleyBasedEraBabbage",
6+
"error": [
7+
{
8+
"contents": {
9+
"contents": "AlonzoInBabbageUtxoPredFailure (ValueNotConservedUTxO (MaryValue (Coin 0) (MultiAsset (fromList []))) (MaryValue (Coin 300000000000) (MultiAsset (fromList []))))",
10+
"tag": "UtxoFailure"
11+
},
12+
"tag": "UtxowFailure"
13+
},
14+
{
15+
"contents": {
16+
"contents": "AlonzoInBabbageUtxoPredFailure (BadInputsUTxO (fromList [TxIn (TxId {unTxId = SafeHash \"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"}) (TxIx 0)]))",
17+
"tag": "UtxoFailure"
18+
},
19+
"tag": "UtxowFailure"
20+
}
21+
],
22+
"kind": "ShelleyTxValidationError"
23+
},
24+
"tag": "TxValidationErrorInCardanoMode"
25+
},
26+
"tag": "TxCmdTxSubmitValidationError"
27+
},
28+
"tag": "TxSubmitFail"
29+
}

nix/haskell.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ let
197197
"cardano-testnet/test/cardano-testnet-golden/files/golden/mary_node_default_config.json"
198198
"cardano-testnet/test/cardano-testnet-golden/files/golden/shelley_node_default_config.json"
199199
"cardano-testnet/test/cardano-testnet-golden/files/golden/shelley_node_default_config.json"
200-
"cardano-testnet/test/cardano-testnet-test/files/golden/tx.failed.response.golden"
200+
"cardano-testnet/test/cardano-testnet-test/files/golden/tx.failed.response.json.golden"
201201
"cardano-testnet/files/data/alonzo/genesis.alonzo.spec.json"
202202
"cardano-testnet/files/data/conway/genesis.conway.spec.json"
203203
];

0 commit comments

Comments
 (0)