{"record":{"id":"4c211c64f9648c43","repo":"instructure/canvas-lms","slug":"token-refresh-failed","errorCode":null,"errorMessage":"Token refresh failed","messagePattern":"Token refresh failed","errorType":"http","errorClass":"LlmConversation::Errors::ConversationError","httpStatus":null,"severity":"error","filePath":"lib/llm_conversation/http_client.rb","lineNumber":81,"sourceCode":"\n    def refresh_v2_token!\n      refresh_token = @account.settings.dig(:llm_conversation_service, :refresh_jwt_token)\n      raise LlmConversation::Errors::ConversationError, \"No refresh token available for account\" if refresh_token.blank?\n\n      uri = URI(\"#{@base_url}/token/refresh\")\n      http = Net::HTTP.new(uri.host, uri.port)\n      if uri.scheme.casecmp?(\"https\")\n        http.use_ssl = true\n        http.verify_mode = OpenSSL::SSL::VERIFY_PEER\n      end\n\n      req = Net::HTTP::Post.new(uri.request_uri,\n                                \"Content-Type\" => \"application/json\",\n                                \"Authorization\" => \"Bearer #{refresh_token}\",\n                                \"x-account-id\" => @account.uuid)\n\n      response = http.request(req)\n      raise LlmConversation::Errors::ConversationError, \"Token refresh failed\" unless response.is_a?(Net::HTTPSuccess)\n\n      result = JSON.parse(response.body)\n      new_api_token = result[\"api_token\"]\n      new_refresh_token = result[\"refresh_token\"]\n\n      @account.settings[:llm_conversation_service] = {\n        api_jwt_token: new_api_token,\n        refresh_jwt_token: new_refresh_token\n      }\n      @account.save!\n\n      LlmConversation::TokenCache.set_api_token(@account, new_api_token)\n      @bearer_token = new_api_token\n    end\n\n    def resolve_base_url\n      region = ApplicationController.region\n      test_cluster = ApplicationController.test_cluster_name","sourceCodeStart":63,"sourceCodeEnd":99,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/lib/llm_conversation/http_client.rb#L63-L99","documentation":"refresh_v2_token! POSTs to the LLM conversation service's /token/refresh endpoint with the account's refresh JWT. If the HTTP response is not a Net::HTTPSuccess (2xx), the client raises ConversationError 'Token refresh failed', meaning the service refused to rotate the token pair.","triggerScenarios":"The /token/refresh call returns a non-2xx status — typically because the refresh JWT is expired or revoked, the x-account-id header does not match a known account, or the LLM conversation service itself is erroring.","commonSituations":"Refresh token expired after long inactivity (no grace refresh); tokens rotated elsewhere making the stored refresh token stale; service-side outage or 5xx; account UUID mismatch between Canvas and the LLM service; wrong llm_conversation_base_url Setting pointing at the wrong environment.","solutions":["Check the refresh endpoint response/status in logs to see why the service rejected it (expired token vs. server error).","Re-provision the account's token pair: complete a fresh initial token exchange and store both api_jwt_token and refresh_jwt_token in account.settings, then clear LlmConversation::TokenCache for the account.","Verify Setting llm_conversation_base_url_<region> points to the correct service environment and that the account UUID matches.","Retry after confirming service health if the cause was a transient 5xx."],"exampleFix":"# before (stale tokens)\n# no re-provisioning; every request 401s, refresh also fails\n\n# after: re-run token exchange, then\naccount.settings[:llm_conversation_service] = {\n  api_jwt_token: new_result[\"api_token\"],\n  refresh_jwt_token: new_result[\"refresh_token\"]\n}\naccount.save!\nLlmConversation::TokenCache.set_api_token(account, new_result[\"api_token\"])","handlingStrategy":"retry","validationCode":"# no pre-call validation possible; refresh failure is server-side\n# ensure base URL and account uuid are correct first:\nraise 'bad base url' unless Setting.get(\"llm_conversation_base_url_#{region}\", nil).present?","typeGuard":null,"tryCatchPattern":"begin\n  client.get(path)\nrescue LlmConversation::Errors::ConversationError => e\n  if e.message == \"Token refresh failed\"\n    LlmConversation::TokenService.reprovision!(account)\n    retry\n  end\n  raise\nend","preventionTips":["Schedule proactive token refresh before the refresh token expires","Monitor /token/refresh failure rates and 5xx from the LLM service","Confirm account UUIDs match between Canvas and the LLM service","Pin llm_conversation_base_url settings per region/environment"],"tags":["ruby","auth","http","token-refresh","api"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}