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

  1. Correct the offset/split logic so string slices start and end on code-point boundaries (use offsetByCodePoints or check Character.isHighSurrogate at cut points).
  2. Strip lone surrogates from input before escaping (regex or codePoints().filter).
  3. 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 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


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)