instructure/canvas-lms · error · ArgumentError

Cannot decode nil token string

Error message

Cannot decode nil token string

What it means

CanvasSecurity::ServicesJwt wraps a raw JWT string for decoding. A nil token string cannot be decoded, so the constructor immediately raises ArgumentError rather than failing later inside JWT parsing. This is a fail-fast guard at the API boundary.

Solutions

  1. Check the token for nil before constructing ServicesJwt and return 401/handle the missing-token case
  2. Fix the token extraction so empty/missing tokens are rejected earlier
  3. Use safe navigation with a fallback or early return in the controller

Example fix

// before
token = CanvasSecurity::ServicesJwt.new(request.headers['Authorization']&.split(' ')&.last)
// after
raw = request.headers['Authorization']&.split(' ')&.last
return render json: { error: 'missing token' }, status: :unauthorized if raw.blank?
token = CanvasSecurity::ServicesJwt.new(raw)
Defensive patterns

Strategy: type-guard

Validate before calling

raise ArgumentError, 'token required' if token_string.nil? || token_string.empty?

Type guard

def present_token?(raw) = raw.is_a?(String) && !raw.empty?

Try / catch

begin
  CanvasSecurity::ServicesJwt.new(raw)
rescue ArgumentError
  respond_missing_token
end

Prevention

When it happens

Trigger: Calling ServicesJwt.new(nil) or ServicesJwt.new(nil, wrapped: false); passing a variable from request headers/session that is nil (e.g. Authorization header missing).

Common situations: Extracting a token with `request.headers['Authorization']&.split(' ')&.last` which yields nil when the header is absent or malformed; cached lookups returning nil.

Related errors


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

Appendix: source

Thrown at gems/canvas_security/lib/canvas_security/services_jwt.rb:33

# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.

class CanvasSecurity::ServicesJwt
  KeyStorage = CanvasSecurity::KeyStorage.new("services-jwt")

  class InvalidRefresh < RuntimeError; end

  REFRESH_WINDOW = 6.hours
  DEFAULT_AUDIENCE = "Instructure"

  attr_reader :token_string, :is_wrapped

  def initialize(raw_token_string, wrapped: true)
    @is_wrapped = wrapped
    if raw_token_string.nil?
      raise ArgumentError, "Cannot decode nil token string"
    end

    @token_string = raw_token_string
  end

  def wrapper_token
    return {} unless is_wrapped

    raw_wrapper_token = CanvasSecurity.base64_decode(token_string)
    keys = [signing_secret]
    keys << previous_signing_secret if previous_signing_secret
    CanvasSecurity.decode_jwt(raw_wrapper_token, keys)
  end

  def original_token(ignore_expiration: false)
    original_crypted_token = if is_wrapped
                               wrapper_token[:user_token]
                             else

View on GitHub (pinned to 1c9f0bb801)