instructure/canvas-lms · error · LlmConversation::Errors::ConversationError
No refresh token available for account
Error message
No refresh token available for account
What it means
In v2 auth mode, HttpClient refreshes expired API tokens by POSTing to /token/refresh with the refresh JWT stored at account.settings[:llm_conversation_service][:refresh_jwt_token]. refresh_v2_token! raises ConversationError if that stored refresh token is blank, because it cannot authenticate the refresh call.
Solutions
- Seed the account's refresh token: set account.settings[:llm_conversation_service] = { api_jwt_token: ..., refresh_jwt_token: ... } (e.g. by running the initial token exchange) and save the account.
- Verify the settings hash shape with account.settings.dig(:llm_conversation_service, :refresh_jwt_token) — fix any key renaming or nesting mismatches.
- Re-run whatever bootstrap/provisioning flow issues the first token pair for the account.
- Check for code paths that overwrite account.settings[:llm_conversation_service] and preserve refresh_jwt_token.
Example fix
# before (account settings)
account.settings[:llm_conversation_service] = { api_jwt_token: "eyJ..." }
account.save!
# after
account.settings[:llm_conversation_service] = {
api_jwt_token: "eyJ...",
refresh_jwt_token: "eyJ..." # required for refresh_v2_token!
}
account.save! Defensive patterns
Strategy: validation
Validate before calling
refresh_token = account.settings.dig(:llm_conversation_service, :refresh_jwt_token) raise 'provision account tokens first' if refresh_token.blank? client = LlmConversation::HttpClient.new(account: account)
Type guard
def v2_tokens_provisioned?(account) account.settings.dig(:llm_conversation_service, :refresh_jwt_token).present? end
Try / catch
begin
client.get(path)
rescue LlmConversation::Errors::ConversationError => e
if e.message == "No refresh token available for account"
LlmConversation::TokenService.provision!(account)
retry
end
raise
end Prevention
- Provision both api_jwt_token and refresh_jwt_token when enabling v2 auth on an account
- Avoid overwriting account.settings[:llm_conversation_service] wholesale; merge keys instead
- Alert on accounts with the v2 flag but missing refresh tokens
When it happens
Trigger: A v2-auth account (ai_experiences_v2_auth enabled) gets a 401 during request(), triggering refresh_v2_token!, but account.settings[:llm_conversation_service][:refresh_jwt_token] was never set or has been cleared.
Common situations: Fresh account that never completed the initial token exchange, so only the API token (or nothing) was stored; settings hash wiped or overwritten by another integration; the refresh token key stored under a different name/shape after a code change; TokenCache invalidated while the account record lacks the refresh token.
Related errors
- Token refresh failed
- Bearer token not configured for LLM Conversation Service
- Cannot use initial token: account does not have…
- A new_id, '# ', referenced an existing # and the # with #…
- A new_integration_id, '#
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/40f241cc5b02fa29.
Report an issue: GitHub.
Appendix: source
Thrown at lib/llm_conversation/http_client.rb:66
end
def post(path, payload: nil)
request(:post, path, payload:)
end
def patch(path, payload: nil)
request(:patch, path, payload:)
end
def delete(path)
request(:delete, path)
end
private
def refresh_v2_token!
refresh_token = @account.settings.dig(:llm_conversation_service, :refresh_jwt_token)
raise LlmConversation::Errors::ConversationError, "No refresh token available for account" if refresh_token.blank?
uri = URI("#{@base_url}/token/refresh")
http = Net::HTTP.new(uri.host, uri.port)
if uri.scheme.casecmp?("https")
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
end
req = Net::HTTP::Post.new(uri.request_uri,
"Content-Type" => "application/json",
"Authorization" => "Bearer #{refresh_token}",
"x-account-id" => @account.uuid)
response = http.request(req)
raise LlmConversation::Errors::ConversationError, "Token refresh failed" unless response.is_a?(Net::HTTPSuccess)
result = JSON.parse(response.body)
new_api_token = result["api_token"]View on GitHub (pinned to 1c9f0bb801)