{"record":{"id":"97d56f42ed419354","repo":"instructure/canvas-lms","slug":"failed-to-unescape-value-str-inspect-e-message-using","errorCode":null,"errorMessage":"Failed to unescape value: #{str.inspect} - #{e.message}. Using original value.","messagePattern":"Failed to unescape value: #(.+?) - #(.+?)\\. Using original value\\.","errorType":"console","errorClass":null,"httpStatus":null,"severity":"info","filePath":"app/services/rubric_llm_service.rb","lineNumber":818,"sourceCode":"  # Quote a value as a JSON string without the surrounding quotes escaping issues.\n  #\n  # Example:\n  #   escape_value(%{She said \"hi\"}) # => \"\\\"She said \\\\\\\"hi\\\\\\\"\\\"\"\n  #   (and we later strip the outer quotes when re-parsing)\n  def escape_value(str)\n    return \"\" if str.nil?\n\n    JSON.generate(str.to_s)[1..-2]\n  end\n\n  # Reverse of escape_value – interpret a line value back into plain text.\n  # If JSON parsing fails (e.g., malformed escape sequences from LLM), returns the original string.\n  def unescape_value(str)\n    return \"\" if str.nil?\n\n    JSON.parse(\"\\\"#{str}\\\"\")\n  rescue JSON::ParserError => e\n    Rails.logger.warn(\"Failed to unescape value: #{str.inspect} - #{e.message}. Using original value.\")\n    str.to_s\n  end\n\n  # Reserve IDs from existing criteria/ratings to avoid collisions when\n  # creating new ones (e.g., mapping _new_* placeholders later).\n  def reserve_existing_ids!(criteria_array)\n    criteria_array.each do |c|\n      cid = (c[:id] || c[\"id\"]).to_s\n      @used_ids[cid] = true if cid.present?\n\n      ratings_raw = c[:ratings] || c[\"ratings\"]\n      normalize_ratings_array(ratings_raw).each do |r|\n        rid = (r[:id] || r[\"id\"]).to_s\n        @used_ids[rid] = true if rid.present?\n      end\n    end\n  end\n","sourceCodeStart":800,"sourceCodeEnd":836,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/services/rubric_llm_service.rb#L800-L836","documentation":"RubricLlmService#unescape_value attempts JSON.parse(\"\\\"#{str}\\\"\") to decode escape sequences (\\n, \\\" etc.) in LLM-produced strings. If the string contains malformed escape sequences, JSON::ParserError is rescued and a warning 'Failed to unescape value: ... Using original value.' is logged, returning the original string. The pipeline continues with the un-unescaped value; nothing raises.","triggerScenarios":"Called from text_to_rubric and text_to_criterion_update on any LLM string field containing invalid JSON escapes — e.g. a literal backslash not part of a valid escape (\\', \\\\x), a stray backslash before a quote, or control characters that JSON cannot parse.","commonSituations":"LLM emits Windows-style paths (C:\\Users\\...), LaTeX/math with backslashes, or code snippets inside rubric criteria/ratings descriptions; prompt-injected or free-form text copied into rubric fields; switching models changes escaping behavior.","solutions":["No action strictly needed — the method already falls back to the original string; check logs if unescaped backslashes end up rendered oddly in saved rubrics.","Sanitize the LLM output before unescape_value (escape lone backslashes: str.gsub(/\\\\(?![\"\\\\/bfnrtu])/, '\\\\\\\\')).","Constrain the model to emit valid JSON (structured output / JSON mode) so escape sequences are well-formed at the source.","If the fallback is unacceptable, raise or mark the field as needing review instead of silently storing the raw value."],"exampleFix":"// before\nJSON.parse(\"\\\"#{str}\\\"\")\nrescue JSON::ParserError => e\n  Rails.logger.warn(\"Failed to unescape value: #{str.inspect} - #{e.message}. Using original value.\")\n  str.to_s\n\n// after: repair lone backslashes before parsing\nrepaired = str.to_s.gsub(/\\\\(?![\"\\\\/bfnrtu])/) { '\\\\\\\\' }\nJSON.parse(\"\\\"#{repaired}\\\"\")\nrescue JSON::ParserError => e\n  Rails.logger.warn(\"Failed to unescape value: #{str.inspect} - #{e.message}. Using original value.\")\n  str.to_s","handlingStrategy":"fallback","validationCode":"// pre-validate LLM string before unescape_value\nvalid = begin\n  JSON.parse(\"\\\"#{str}\\\"\")\n  true\nrescue JSON::ParserError\n  false\nend\nRails.logger.debug(\"unescape will fall back for: #{str.inspect}\") unless valid","typeGuard":"def unescapable?(str)\n  return true if str.nil?\n  !str.match?(/\\\\(?![\"\\\\/bfnrtu])/)\nend","tryCatchPattern":"// The method already swallows the error; wrap calls if you need the fallback signal:\nraw = llm_output.dig('criteria', 0, 'description')\ndescription = str.is_a?(String) ? service.public_unescape_value(raw) : \"\"","preventionTips":["Request strict JSON output from the model so escape sequences are well-formed.","Normalize/escape lone backslashes in LLM text before running unescape_value.","Expect rendered text to contain raw backslashes when this warning fires; test rubric descriptions for stray '\\\\'.","Treat the fallback (original string) as acceptable, but log sampled values to catch recurring escaping pathologies."],"tags":["json","escaping","llm-output","fallback"],"backgroundTag":"json-parse-error","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"}