Skip to content
This repository was archived by the owner on Aug 28, 2025. It is now read-only.

Commit 998a008

Browse files
feat: allow optional "error_code" field in responses (#75)
* feat: allow optional "error_code" field in responses This change relaxes the ic-ref-test suite to allow optional "error_code" field in query responses and in request statuses as described in dfinity/interface-spec#38 We also emit error codes in the reference implementation.
1 parent 2732052 commit 998a008

File tree

8 files changed

+105
-56
lines changed

8 files changed

+105
-56
lines changed

src/IC/Debug/JSON.hs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ instance ToJSON RejectCode where
8080
toJSON = genericToJSON customOptions
8181
toEncoding = genericToEncoding customOptions
8282

83+
deriving instance Generic ErrorCode
84+
instance ToJSON ErrorCode where
85+
toJSON = genericToJSON customOptions
86+
toEncoding = genericToEncoding customOptions
87+
8388
deriving instance Generic Response
8489
instance ToJSON Response where
8590
toJSON = genericToJSON customOptions

src/IC/HTTP/Request.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,8 @@ entitiyId = fmap EntityId <$> blob
145145

146146
-- Printing responses
147147
response :: ReqResponse -> GenR
148-
response (QueryResponse (Rejected (c, s))) = rec
148+
response (QueryResponse (Rejected (c, s, err))) = rec $
149+
[ "error_code" =: GText (T.pack $ errorCode c) | c <- maybeToList err] ++
149150
[ "status" =: GText "rejected"
150151
, "reject_code" =: GNat (fromIntegral (rejectCode c))
151152
, "reject_message" =: GText (T.pack s)

src/IC/Ref.hs

Lines changed: 55 additions & 47 deletions
Large diffs are not rendered by default.

src/IC/Serialise.hs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ instance Serialise NeedsToRespond where
4747
deriving instance Generic RejectCode
4848
instance Serialise RejectCode where
4949

50+
deriving instance Generic ErrorCode
51+
instance Serialise ErrorCode where
52+
5053
deriving instance Generic Response
5154
instance Serialise Response where
5255

src/IC/Test/Agent.hs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ validateStateCert' what cert = do
402402
validateStateCert :: (HasCallStack, HasAgentConfig) => Certificate -> IO ()
403403
validateStateCert = validateStateCert' "certificate"
404404

405-
data ReqResponse = Reply Blob | Reject Natural T.Text
405+
data ReqResponse = Reply Blob | Reject Natural T.Text (Maybe T.Text)
406406
deriving (Eq, Show)
407407
data ReqStatus = Processing | Pending | Responded ReqResponse | UnknownStatus
408408
deriving (Eq, Show)
@@ -415,6 +415,14 @@ prettyBlob x =
415415
let s = map (chr . fromIntegral) (BS.unpack x) in
416416
if all isPrint s then s else asHex x
417417

418+
maybeCertValue :: HasCallStack => CertVal a => Certificate -> [Blob] -> IO (Maybe a)
419+
maybeCertValue cert path = case lookupPath (cert_tree cert) path of
420+
Found b -> case fromCertVal b of
421+
Just x -> return (Just x)
422+
Nothing -> assertFailure $ "Cannot parse " ++ prettyPath path ++ " from " ++ show b
423+
Absent -> return Nothing
424+
x -> assertFailure $ "Expected to find " ++ prettyPath path ++ ", but got " ++ show x
425+
418426
certValue :: HasCallStack => CertVal a => Certificate -> [Blob] -> IO a
419427
certValue cert path = case lookupPath (cert_tree cert) path of
420428
Found b -> case fromCertVal b of
@@ -444,7 +452,8 @@ getRequestStatus sender cid rid = do
444452
certValueAbsent cert ["request_status", rid, "reply"]
445453
code <- certValue cert ["request_status", rid, "reject_code"]
446454
msg <- certValue cert ["request_status", rid, "reject_message"]
447-
return $ Responded (Reject code msg)
455+
errorCode <- maybeCertValue cert ["request_status", rid, "error_code"]
456+
return $ Responded (Reject code msg errorCode)
448457
Found s -> assertFailure $ "Unexpected status " ++ show s
449458
-- This case should not happen with a compliant IC, but let
450459
-- us be liberal here, and strict in a dedicated test
@@ -512,13 +521,14 @@ queryResponse = asExceptT . record do
512521
"rejected" -> do
513522
code <- field nat "reject_code"
514523
msg <- field text "reject_message"
515-
return $ Reject code msg
524+
error_code <- optionalField text "error_code"
525+
return $ Reject code msg error_code
516526
_ -> lift $ throwError $ "Unexpected status " <> T.pack (show s)
517527

518528
isReject :: HasCallStack => [Natural] -> ReqResponse -> IO ()
519529
isReject _ (Reply r) =
520530
assertFailure $ "Expected reject, got reply:" ++ prettyBlob r
521-
isReject codes (Reject n msg) = do
531+
isReject codes (Reject n msg _) = do
522532
assertBool
523533
("Reject code " ++ show n ++ " not in " ++ show codes ++ "\n" ++ T.unpack msg)
524534
(n `elem` codes)
@@ -534,8 +544,9 @@ isErrOrReject codes (Right res) = isReject codes res
534544

535545
isReply :: HasCallStack => ReqResponse -> IO Blob
536546
isReply (Reply b) = return b
537-
isReply (Reject n msg) =
538-
assertFailure $ "Unexpected reject (code " ++ show n ++ "): " ++ T.unpack msg
547+
isReply (Reject n msg error_code) =
548+
assertFailure $ "Unexpected reject (code " ++ show n ++ (maybe "" showErrCode error_code) ++ "): " ++ T.unpack msg
549+
where showErrCode ec = ", error_code: " ++ T.unpack ec
539550

540551
-- Convenience decoders
541552

src/IC/Test/Spec.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2692,7 +2692,7 @@ isRelay = runGet $ Get.getWord32le >>= \case
26922692
0x4c444944 -> fail "Encountered Candid when expectin relayed data. Did you forget to use isRelay?"
26932693
c -> do
26942694
msg <- Get.getRemainingLazyByteString
2695-
return $ Reject (fromIntegral c) (T.decodeUtf8With T.lenientDecode (BS.toStrict msg))
2695+
return $ Reject (fromIntegral c) (T.decodeUtf8With T.lenientDecode (BS.toStrict msg)) Nothing
26962696

26972697

26982698
-- Shortcut for test cases that just need one canister.

src/IC/Types.hs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import Data.List
2020
import Data.List.Split (chunksOf)
2121
import Numeric.Natural
2222
import Control.Monad.Except
23+
import Text.Printf (printf)
2324

2425
type () = M.Map
2526

@@ -72,6 +73,26 @@ rejectCode RC_DESTINATION_INVALID = 3
7273
rejectCode RC_CANISTER_REJECT = 4
7374
rejectCode RC_CANISTER_ERROR = 5
7475

76+
data ErrorCode
77+
= EC_CANISTER_NOT_FOUND
78+
| EC_METHOD_NOT_FOUND
79+
| EC_CANISTER_EMPTY
80+
| EC_CANISTER_NOT_EMPTY
81+
| EC_CANISTER_STOPPED
82+
| EC_CANISTER_NOT_STOPPED
83+
| EC_CANISTER_NOT_RUNNING
84+
| EC_CANISTER_RESTARTED
85+
| EC_CANISTER_TRAPPED
86+
| EC_CANISTER_REJECTED
87+
| EC_CANISTER_DID_NOT_REPLY
88+
| EC_INVALID_ENCODING
89+
| EC_INVALID_ARGUMENT
90+
| EC_INVALID_MODULE
91+
| EC_NOT_AUTHORIZED
92+
deriving (Show, Enum)
93+
94+
errorCode :: ErrorCode -> String
95+
errorCode = printf "ICHS%04d" . fromEnum
7596

7697
data Response = Reply Blob | Reject (RejectCode, String)
7798
deriving Show

src/ic-ref-run.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ printQueryRequest (QueryRequest _ _ method arg) =
5959
printf "→ query %s%s\n" method (shorten 60 (candidOrPretty arg))
6060

6161
printCallResponse :: CallResponse -> IO ()
62-
printCallResponse (Rejected (c, s)) =
62+
printCallResponse (Rejected (c, s, _err)) =
6363
printf "← rejected (%s): %s\n" (show c) s
6464
printCallResponse (Replied blob) =
6565
printf "← replied: %s\n" (shorten 100 (candidOrPretty blob))

0 commit comments

Comments
 (0)