spring-projects/spring-security · error · IllegalArgumentException
Unexpected low surrogate character, value =
Error message
Unexpected low surrogate character, value = <ch>
What it means
TextEscapeUtils.escapeEntities() throws this when a low surrogate (U+DC00–U+DFFF) appears without a preceding high surrogate. A lone low surrogate is not a valid standalone character, so the method fails fast instead of escaping an invalid value.
Solutions
- Correct the offset/split logic so string slices start and end on code-point boundaries (use offsetByCodePoints or check Character.isHighSurrogate at cut points).
- Strip lone surrogates from input before escaping (regex or codePoints().filter).
- Fix the decoder that produced invalid UTF-16 data at the source.
Example fix
// before String escaped = TextEscapeUtils.escapeEntities(input.substring(1)); // after int start = 1; if (Character.isLowSurrogate(input.charAt(start))) start--; String escaped = TextEscapeUtils.escapeEntities(input.substring(start));
Defensive patterns
Strategy: validation
Validate before calling
public static boolean startsOnCodePointBoundary(String s) {
return s.isEmpty() || !Character.isLowSurrogate(s.charAt(0));
}
// apply to every substring/slice fed into escapeEntities
Type guard
public static String stripLoneLowSurrogates(String s) {
return s.codePoints().filter(cp -> !(cp >= 0xDC00 && cp <= 0xDFFF))
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString();
}
Try / catch
try {
escaped = TextEscapeUtils.escapeEntities(slice);
} catch (IllegalArgumentException e) {
if (!e.getMessage().startsWith("Unexpected low surrogate")) throw e;
escaped = TextEscapeUtils.escapeEntities(stripLoneLowSurrogates(slice));
}
Prevention
- When calling substring with an offset derived by counting chars, verify the boundary with Character.isLowSurrogate
- Use s.offsetByCodePoints() to compute valid cut positions
- Treat any low surrogate appearing at a string start or after a non-surrogate char as data corruption and fix the producer
- Test string-processing code with emoji and other supplementary characters
When it happens
Trigger: Calling escapeEntities(s) where s starts with or contains a low surrogate not preceded by a high surrogate — typically from slicing a string starting mid-pair (e.g. s.substring(1) where s starts with an emoji) or corrupt decoding.
Common situations: substring/indexOf offsets off by one relative to a supplementary character; concatenating string fragments split inside a code point; malformed data read from network or files.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
- Expected low surrogate character but found value =
- Missing low surrogate character at end of string
- Amount of performance parameters invalid
- authorizationManagerFactory must be an instance of…
- Bad number of rounds
AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10).
Data as JSON: /api/errors/9d415927e50d1ead.
Report an issue: GitHub.
Appendix: source
Thrown at web/src/main/java/org/springframework/security/web/util/TextEscapeUtils.java:71
}
else if (Character.isHighSurrogate(ch)) {
if (i + 1 >= s.length()) {
// Unexpected end
throw new IllegalArgumentException("Missing low surrogate character at end of string");
}
char low = s.charAt(i + 1);
if (!Character.isLowSurrogate(low)) {
throw new IllegalArgumentException(
"Expected low surrogate character but found value = " + (int) low);
}
int codePoint = Character.toCodePoint(ch, low);
if (Character.isDefined(codePoint)) {
sb.append("&#").append(codePoint).append(";");
}
i++; // skip the next character as we have already dealt with it
}
else if (Character.isLowSurrogate(ch)) {
throw new IllegalArgumentException("Unexpected low surrogate character, value = " + (int) ch);
}
else if (Character.isDefined(ch)) {
sb.append("&#").append((int) ch).append(";");
}
// Ignore anything else
}
return sb.toString();
}
}
View on GitHub (pinned to 96852e8860)