You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Aeson解码非穷举模式错误:Scotty应用失败但Repl正常

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 = []}))

排查方向

  1. 补全非穷尽模式处理: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值类型,定位问题根源。

  2. 检查响应格式差异:Scotty中实际接收的响应可能与testResponse存在细微差异——比如Cloudflare在异常场景下返回不同结构的JSON,或响应未正确解压(如gzip压缩未处理)导致解析出错误的JSON值。

  3. 修正键值类型匹配: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) 
    
  4. 验证响应读取逻辑:确认Scotty中读取Cloudflare响应的方式是否正确——若响应体被读取为字符串,需先将字符串解码为JSON值,再解析为CFUploadResponse,而非直接对字符串执行解码操作。

内容的提问来源于stack exchange,提问作者jxxcarlson

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 20:47:54