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

Rails集成NetDocuments API的Invalid Grant错误处理及令牌管理

解决方案:NetDocuments API令牌管理的竞态条件与错误处理

你遇到的Invalid Grant错误核心是多线程/多进程环境下的竞态条件——多个进程同时尝试用同一个刷新令牌请求新令牌,第一个请求成功后原刷新令牌失效,后续请求就会失败。下面针对你的问题给出具体解决方案:

1. 多线程/多进程环境下确保刷新令牌仅使用一次

解决这个问题的核心是数据库级别的排他锁,Rails内存锁(如Mutex)无法跨进程生效,必须借助数据库行锁保证同一时间只有一个进程能执行刷新逻辑。

修改你的代码,用with_lock包裹刷新逻辑,同时在锁内部二次检查过期时间(避免等待锁期间凭证已被其他进程更新):

def self.netdocuments_credential
  credential = Credential.find_by(kind: 'netdocuments')
  return nil unless credential.present?

  # 数据库行锁:跨进程生效,同一时间仅一个进程能进入代码块
  credential.with_lock do
    # 二次检查过期时间,避免等待锁期间凭证已被刷新
    if Time.now > credential.expires_at
      resp = Faraday.post("#{API_BASE_URL[get_data_region]}/v1#{TOKEN_PATH}") do |req|
        authorization_string = Base64.encode64("#{CLIENT_ID[get_data_region]}:#{CLIENT_SECRET[get_data_region]}").gsub(/\n/, '')
        req.headers['Authorization'] = "Basic " + authorization_string
        req.body = { grant_type: "refresh_token", refresh_token: credential.refresh_token }
      end

      begin
        response_body = JSON.parse(resp.body)
        if resp.success?
          access_token = response_body["access_token"]
          refresh_token = response_body["refresh_token"]
          credential.update!(
            token: access_token,
            refresh_token: refresh_token,
            expires_at: Time.now + 3600
          )
        else
          # 主动处理API返回的错误(如Invalid Grant)
          error_msg = response_body['error_description'] || resp.body
          Rails.logger.error "NetDocuments令牌刷新失败: #{error_msg}"
          handle_invalid_grant(credential)
        end
      rescue JSON::ParserError => e
        Rails.logger.error "解析NetDocuments响应失败: #{e.message}"
        handle_invalid_grant(credential)
      end
    end
  end

  credential
end

# 新增Invalid Grant错误处理方法,替换直接删除凭证的逻辑
def self.handle_invalid_grant(credential)
  # 优先尝试自动恢复:比如用服务账号的client_credentials模式重新获取令牌(如果API支持)
  # 示例逻辑(需根据NetDocuments服务账号流程调整):
  # recovery_resp = Faraday.post(TOKEN_URL) do |req|
  #   req.body = { grant_type: "client_credentials", scope: "..." }
  #   ...
  # end
  # 如果自动恢复失败,再标记凭证无效而非直接删除
  credential.update!(invalid: true)
  raise StandardError, "NetDocuments令牌失效,正在自动恢复,请稍后重试。"
end

关键改动:

  • 用with_lock实现跨进程的排他锁,彻底避免并发刷新
  • 锁内部二次校验过期时间,避免无效刷新操作
  • 新增错误分支处理API返回的失败响应,替换粗暴删除凭证的逻辑
  • 提供自动恢复的扩展入口,减少用户手动干预

2. 手动设置1小时过期时间是否会引发Invalid Grant?

这个不是直接原因。NetDocuments访问令牌默认有效期就是1小时,你手动设置expires_at = Time.now + 3600是符合API规范的。

如果想进一步降低并发刷新概率,可以提前5-10分钟刷新令牌(比如设置expires_at = Time.now + 3540),这样即使有少量线程同时检查,也会在令牌真正过期前完成刷新,减少并发触发的场景。

额外优化建议

  • 增加日志维度:记录刷新操作的进程ID、线程ID、时间,方便排查竞态问题
  • 实现退避机制:遇到Invalid Grant时,等待几秒后再重试,避免重复无效请求
  • 分布式锁备选:如果应用部署在多台服务器,数据库行锁依然有效,也可以用Redis分布式锁(如redlock-rb gem)增强灵活性

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 05:28:10