Aeson解码非穷举模式错误:Scotty应用失败但Repl正常
问题概述
在Scotty服务应用中使用Aeson解码Cloudflare上传响应时,触发非穷尽模式错误:
src/CFUpload.hs:(66,6)-(71,27): Non-exhaustive patterns in function parseJSON
错误指向CFUploadResponse的FromJSON实例代码段,但将相同响应数据拿到REPL中测试解码器,却能成功完成解码。
代码场景
Scotty服务核心流程:
- 接收POST请求中的图片数据
- 下载图片资源
- 获取Cloudflare上传URL
- 将图片上传至Cloudflare
- 接收Cloudflare返回的响应
- 尝试用自定义解码器解析响应(此处触发报错)
- 返回处理结果
将解析步骤替换为直接返回响应内容,得到的JSON字符串(记为testResponse):
"{\n \"result\": {\n \"id\": \"673996fb-4d26-4332-6e6b-e8bf7b608500\",\n \"filename\": \"bird2.jpg\",\n \"uploaded\": \"2023-03-18T22:53:56.705Z\",\n \"requireSignedURLs\": false,\n \"variants\": [\n \"https://imagedelivery.net/9U-0Y4sEzXlO6BXzTnQnYQ/673996fb-4d26-4332-6e6b-e8bf7b608500/public\"\n ]\n },\n \"success\": true,\n \"errors\": [],\n \"messages\": []\n}"
格式化后结构:
{ "result": { "id": "673996fb-4d26-4332-6e6b-e8bf7b608500", "filename": "bird2.jpg", "uploaded": "2023-03-18T22:53:56.705Z", "requireSignedURLs": false, "variants": [ "https://imagedelivery.net/9U-0Y4sEzXlO6BXzTnQnYQ/673996fb-4d26-4332-6e6b-e8bf7b608500/public" ] }, "success": true, "errors": [], "messages": [] }
类型定义
对应JSON结构的Haskell类型:
data CFUploadResponse = CFUploadResponse { result :: CFUploadResult, success :: Bool, errors :: [String], messages :: [String] } deriving Show data CFUploadResult = CFUploadResult { id :: String, filename :: String, uploaded :: String, requireSignedURLs :: Bool, variants :: [String] } deriving Show
解码器实现
instance FromJSON CFUploadResponse where parseJSON (Object v) = -- (66) CFUploadResponse <$> -- (67) v .: "result" <*> -- (68) v .: "success" <*> -- (69) v .: "errors" <*> -- (70) v .: "messages" -- (71) instance FromJSON CFUploadResult where parseJSON = withObject "CFUploadResult" $ \o -> do id <- o .: Data.Text.pack "id" filename <- o .: Data.Text.pack "filename" uploaded <- o .: Data.Text.pack "uploaded" requireSignedURLs <- o .: Data.Text.pack "requireSignedURLs" variants <- o .: Data.Text.pack "variants" return (CFUploadResult id filename uploaded requireSignedURLs variants)
REPL测试结果
定义解码函数:
myDecode str = Data.Aeson.eitherDecode $ BL.pack str :: Either String (Maybe CFUpload.CFUploadResponse)
在stack repl中执行myDecode testResponse,得到成功解码结果:
Right (Just (CFUploadResponse {result = CFUploadResult {id = "49660d63-a43f-4011-1a7a-ff6435305d00", filename = "bird2.jpg", uploaded = "2023-03-16T23:08:22.768Z", requireSignedURLs = False, variants = ["https://imagedelivery.net/9U-0Y4sEzXlO6BXzTnQnYQ/49660d63-a43f-4011-1a7a-ff6435305d00/public"]}, success = True, errors = [], messages = []}))
排查方向
补全非穷尽模式处理:
CFUploadResponse的parseJSON仅处理了Object类型的JSON值,未覆盖Null、String等其他可能类型。Scotty中可能接收到的响应并非直接的JSON对象(比如响应被包裹为字符串),导致触发非穷尽错误。修改解码器添加错误分支:instance FromJSON CFUploadResponse where parseJSON (Object v) = CFUploadResponse <$> v .: "result" <*> v .: "success" <*> v .: "errors" <*> v .: "messages" parseJSON invalid = fail $ "Expected Object for CFUploadResponse but got: " ++ show invalid此修改可输出实际接收到的JSON值类型,定位问题根源。
检查响应格式差异:Scotty中实际接收的响应可能与
testResponse存在细微差异——比如Cloudflare在异常场景下返回不同结构的JSON,或响应未正确解压(如gzip压缩未处理)导致解析出错误的JSON值。修正键值类型匹配:
CFUploadResult解码器中手动使用Data.Text.pack包裹键名是冗余操作,.:操作符默认接受Text类型键名。若未启用OverloadedStrings扩展,字符串字面量"id"是String类型,与.:要求的Text不匹配,可能导致隐藏错误(REPL环境可能默认启用了该扩展),直接使用"id"即可:instance FromJSON CFUploadResult where parseJSON = withObject "CFUploadResult" $ \o -> do id <- o .: "id" filename <- o .: "filename" uploaded <- o .: "uploaded" requireSignedURLs <- o .: "requireSignedURLs" variants <- o .: "variants" return (CFUploadResult id filename uploaded requireSignedURLs variants)验证响应读取逻辑:确认Scotty中读取Cloudflare响应的方式是否正确——若响应体被读取为字符串,需先将字符串解码为JSON值,再解析为
CFUploadResponse,而非直接对字符串执行解码操作。
内容的提问来源于stack exchange,提问作者jxxcarlson

