instructure/canvas-lms · critical · LlmConversation::Errors::ConversationError

Bearer token not configured for LLM Conversation Service

Error message

Bearer token not configured for LLM Conversation Service

What it means

Every HttpClient#request call first checks that a bearer token was resolved. The constructor picks it from (in order) the credentials initial_token (v2 initial-token mode), TokenCache/v2 account tokens, or credentials.llm_conversation_bearer_token. If all of those resolve to nil, request raises ConversationError because it cannot set the Authorization header.

Solutions

  1. Set credentials.llm_conversation_bearer_token for non-v2 environments: rails credentials:edit, add llm_conversation_bearer_token: <token>.
  2. For v2-auth accounts, ensure LlmConversation::TokenCache.get_api_token can resolve a token — provision the account's api_jwt_token/refresh_jwt_token in account.settings and warm the cache.
  3. Confirm you are in the right Rails environment and that RAILS_MASTER_KEY / credentials file actually contains the LLM keys.
  4. If using use_initial_token, verify Rails.application.credentials.dig(:llm_conversation_service, :initial_token) is set — otherwise the resolved token is nil and every request raises.

Example fix

# before (config/credentials.yml or local)
# llm_conversation_bearer_token: (absent)

# after
llm_conversation_bearer_token: "<service-bearer-token>"
# or for initial-token mode:
llm_conversation_service:
  initial_token: "<initial-token>"
Defensive patterns

Strategy: validation

Validate before calling

token = Rails.application.credentials.llm_conversation_bearer_token
raise 'LLM bearer token not configured' if token.blank?
client = LlmConversation::HttpClient.new(account: account)

Type guard

def client_ready?(client)
  !client.instance_variable_get(:@bearer_token).nil?
rescue
  false
end

Try / catch

begin
  client.get(path)
rescue LlmConversation::Errors::ConversationError => e
  if e.message.include?("Bearer token not configured")
    raise ConfigError, "Set llm_conversation_bearer_token or provision v2 account tokens"
  end
  raise
end

Prevention

When it happens

Trigger: Initializing the client without use_initial_token on an account without ai_experiences_v2_auth when credentials.llm_conversation_bearer_token is nil/empty; or calling get/post/patch/delete on a client whose token lookup returned nil (e.g. TokenCache.get_api_token returned nil and no refresh was attempted because the request guard fires first).

Common situations: Rails credentials missing the llm_conversation_bearer_token key in the environment (missing credentials.yml.local / RAILS_MASTER_KEY mismatch); account has v2 flag but its cached API token expired/was evicted and refresh token storage is broken; new environment or region where credentials were never configured.

Understand the failure class

Background: "API key is required" / "API key not found" / "No API key was set": the missing-api-key error family across 16 libraries — this error's family across 16 libraries.

Related errors


AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15). Data as JSON: /api/errors/4fec2f598218ab2f. Report an issue: GitHub.

Appendix: source

Thrown at lib/llm_conversation/http_client.rb:125

            end

      raise LlmConversation::Errors::ConversationError, base_url_error_message(region, test_cluster) if url.nil?

      url
    end

    def base_url_error_message(region, test_cluster)
      if test_cluster.present? && region.present?
        "None of llm_conversation_base_url_beta_#{region}, llm_conversation_base_url_#{region}, or llm_conversation_base_url setting is configured"
      elsif region.present?
        "Neither llm_conversation_base_url_#{region} nor llm_conversation_base_url setting is configured"
      else
        "llm_conversation_base_url setting is not configured"
      end
    end

    def request(method, path, payload: nil)
      raise LlmConversation::Errors::ConversationError, "Bearer token not configured for LLM Conversation Service" if @bearer_token.nil?

      uri = URI("#{@base_url}#{path}")
      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

      headers = {
        "Content-Type" => "application/json",
        "Authorization" => "Bearer #{@bearer_token}",
        "x-account-id" => @account&.uuid
      }

      req = case method
            when :get
              Net::HTTP::Get.new(uri.request_uri, headers)

View on GitHub (pinned to 1c9f0bb801)