{"record":{"id":"118a9824de36cc25","repo":"instructure/canvas-lms","slug":"cannot-decode-nil-token-string","errorCode":null,"errorMessage":"Cannot decode nil token string","messagePattern":"Cannot decode nil token string","errorType":"validation","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"gems/canvas_security/lib/canvas_security/services_jwt.rb","lineNumber":33,"sourceCode":"# details.\n#\n# You should have received a copy of the GNU Affero General Public License along\n# with this program. If not, see <http://www.gnu.org/licenses/>.\n\nclass CanvasSecurity::ServicesJwt\n  KeyStorage = CanvasSecurity::KeyStorage.new(\"services-jwt\")\n\n  class InvalidRefresh < RuntimeError; end\n\n  REFRESH_WINDOW = 6.hours\n  DEFAULT_AUDIENCE = \"Instructure\"\n\n  attr_reader :token_string, :is_wrapped\n\n  def initialize(raw_token_string, wrapped: true)\n    @is_wrapped = wrapped\n    if raw_token_string.nil?\n      raise ArgumentError, \"Cannot decode nil token string\"\n    end\n\n    @token_string = raw_token_string\n  end\n\n  def wrapper_token\n    return {} unless is_wrapped\n\n    raw_wrapper_token = CanvasSecurity.base64_decode(token_string)\n    keys = [signing_secret]\n    keys << previous_signing_secret if previous_signing_secret\n    CanvasSecurity.decode_jwt(raw_wrapper_token, keys)\n  end\n\n  def original_token(ignore_expiration: false)\n    original_crypted_token = if is_wrapped\n                               wrapper_token[:user_token]\n                             else","sourceCodeStart":15,"sourceCodeEnd":51,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/gems/canvas_security/lib/canvas_security/services_jwt.rb#L15-L51","documentation":"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.","triggerScenarios":"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).","commonSituations":"Extracting a token with `request.headers['Authorization']&.split(' ')&.last` which yields nil when the header is absent or malformed; cached lookups returning nil.","solutions":["Check the token for nil before constructing ServicesJwt and return 401/handle the missing-token case","Fix the token extraction so empty/missing tokens are rejected earlier","Use safe navigation with a fallback or early return in the controller"],"exampleFix":"// before\ntoken = CanvasSecurity::ServicesJwt.new(request.headers['Authorization']&.split(' ')&.last)\n// after\nraw = request.headers['Authorization']&.split(' ')&.last\nreturn render json: { error: 'missing token' }, status: :unauthorized if raw.blank?\ntoken = CanvasSecurity::ServicesJwt.new(raw)","handlingStrategy":"type-guard","validationCode":"raise ArgumentError, 'token required' if token_string.nil? || token_string.empty?","typeGuard":"def present_token?(raw) = raw.is_a?(String) && !raw.empty?","tryCatchPattern":"begin\n  CanvasSecurity::ServicesJwt.new(raw)\nrescue ArgumentError\n  respond_missing_token\nend","preventionTips":["Check Authorization headers for nil/blank before parsing","Use safe navigation and early returns in controllers","Add request specs covering missing-token paths"],"tags":["ruby","jwt","null-check"],"backgroundTag":"null-argument","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"}