instructure/canvas-lms · info
Failed to unescape value: #
Error message
Failed to unescape value: #{str.inspect} - #{e.message}. Using original value. What it means
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.
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.
Example fix
// before
JSON.parse("\"#{str}\"")
rescue JSON::ParserError => e
Rails.logger.warn("Failed to unescape value: #{str.inspect} - #{e.message}. Using original value.")
str.to_s
// after: repair lone backslashes before parsing
repaired = str.to_s.gsub(/\\(?!["\\/bfnrtu])/) { '\\\\' }
JSON.parse("\"#{repaired}\"")
rescue JSON::ParserError => e
Rails.logger.warn("Failed to unescape value: #{str.inspect} - #{e.message}. Using original value.")
str.to_s Defensive patterns
Strategy: fallback
Validate before calling
// pre-validate LLM string before unescape_value
valid = begin
JSON.parse("\"#{str}\"")
true
rescue JSON::ParserError
false
end
Rails.logger.debug("unescape will fall back for: #{str.inspect}") unless valid Type guard
def unescapable?(str) return true if str.nil? !str.match?(/\\(?!["\\/bfnrtu])/) end
Try / catch
// The method already swallows the error; wrap calls if you need the fallback signal:
raw = llm_output.dig('criteria', 0, 'description')
description = str.is_a?(String) ? service.public_unescape_value(raw) : "" Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: JSON parse error: "Unexpected token" / "not valid JSON" / "failed to parse" — what JSON parsers are really complaining about — this error's family across 45 libraries.
Related errors
- [AutoGrade] Criteria count mismatch for submission #
- e.message
- e.message
- Error parsing JSON results from Outcomes Service: #
- filter values must be JSON-compatible types
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/97d56f42ed419354.
Report an issue: GitHub.
Appendix: source
Thrown at app/services/rubric_llm_service.rb:818
# Quote a value as a JSON string without the surrounding quotes escaping issues.
#
# Example:
# escape_value(%{She said "hi"}) # => "\"She said \\\"hi\\\"\""
# (and we later strip the outer quotes when re-parsing)
def escape_value(str)
return "" if str.nil?
JSON.generate(str.to_s)[1..-2]
end
# Reverse of escape_value – interpret a line value back into plain text.
# If JSON parsing fails (e.g., malformed escape sequences from LLM), returns the original string.
def unescape_value(str)
return "" if str.nil?
JSON.parse("\"#{str}\"")
rescue JSON::ParserError => e
Rails.logger.warn("Failed to unescape value: #{str.inspect} - #{e.message}. Using original value.")
str.to_s
end
# Reserve IDs from existing criteria/ratings to avoid collisions when
# creating new ones (e.g., mapping _new_* placeholders later).
def reserve_existing_ids!(criteria_array)
criteria_array.each do |c|
cid = (c[:id] || c["id"]).to_s
@used_ids[cid] = true if cid.present?
ratings_raw = c[:ratings] || c["ratings"]
normalize_ratings_array(ratings_raw).each do |r|
rid = (r[:id] || r["id"]).to_s
@used_ids[rid] = true if rid.present?
end
end
end
View on GitHub (pinned to 1c9f0bb801)