{"record":{"id":"dadcd5a219560672","repo":"quarkusio/quarkus","slug":"invalid-escape-sequence-c-c2-at-index-i","errorCode":null,"errorMessage":"invalid escape sequence `%${c}${c2}' at index ${i-2} of: ${s}","messagePattern":"invalid escape sequence `%(.+?)(.+?)' at index (.+?) of: (.+?)","errorType":"exception","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/URIDecoder.java","lineNumber":81,"sourceCode":"        for (; i < size; i++) {\n            char c = s.charAt(i);\n            if (c == '%') {\n                if (i == size - 1) {\n                    throw new IllegalArgumentException(\"unterminated escape\"\n                            + \" sequence at end of string: \" + s);\n                }\n                c = s.charAt(++i);\n                if (c == '%') {\n                    buf[pos++] = '%'; // \"%%\" -> \"%\"\n                    break;\n                }\n                if (i >= size - 1) {\n                    throw new IllegalArgumentException(\"partial escape\"\n                            + \" sequence at end of string: \" + s);\n                }\n                c = decodeHexNibble(c);\n                final char c2 = decodeHexNibble(s.charAt(++i));\n                if (c == Character.MAX_VALUE || c2 == Character.MAX_VALUE) {\n                    throw new IllegalArgumentException(\n                            \"invalid escape sequence `%\" + s.charAt(i - 1)\n                                    + s.charAt(i) + \"' at index \" + (i - 2)\n                                    + \" of: \" + s);\n                }\n                c = (char) (c * 16 + c2);\n                // shouldn't check for plus since it would be a double decoding\n                buf[pos++] = (byte) c;\n            } else {\n                buf[pos++] = (byte) (plus && c == '+' ? ' ' : c);\n            }\n        }\n        return new String(buf, 0, pos, StandardCharsets.UTF_8);\n    }\n\n    /**\n     * Helper to decode half of a hexadecimal number from a string.\n     *","sourceCodeStart":63,"sourceCodeEnd":99,"githubUrl":"https://github.com/quarkusio/quarkus/blob/e1c734241f34c7919086ceb4c9262b4a58f6de44/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/URIDecoder.java#L63-L99","documentation":"After '%' the decoder reads two characters and converts each with decodeHexNibble. If either character is not a valid hex digit (decodeHexNibble returns Character.MAX_VALUE), this IllegalArgumentException reports the invalid escape, its index, and the full input. It enforces strict RFC-compliant percent-decoding.","triggerScenarios":"Calling decodeURIComponent with an escape whose digits are not hex, e.g. \"%GG\", \"%zz\", \"%20x\" mixed sequences like \"%q1\" — any of the two characters after '%' being non-hex.","commonSituations":"Hand-written or machine-mangled URLs with non-hex after '%'; decoding strings that were never URL-encoded (raw '%' used as literal, e.g. \"100% off\"); template placeholders like '%{var}' passed to the decoder.","solutions":["Percent-encode literal '%' as %25 before decoding","Fix the escape to use only hex characters [0-9A-Fa-f]","Validate with regex /^([^%]|%[0-9A-Fa-f]{2})*$/ before calling the decoder","Catch IllegalArgumentException and return a 400 response with a descriptive message"],"exampleFix":"// before\nURIDecoder.decodeURIComponent(\"search=50% off\");\n// after\nURIDecoder.decodeURIComponent(\"search=50%25 off\");","handlingStrategy":"validation","validationCode":"private static final Pattern HEXESC = Pattern.compile(\"^([^%]|%[0-9A-Fa-f]{2})*$\");\nstatic boolean strictlyEncoded(String s) { return s != null && HEXESC.matcher(s).matches(); }","typeGuard":"static boolean allEscapesAreHex(String s) {\n    for (int i = 0; i < s.length(); i++) {\n        if (s.charAt(i) == '%') {\n            if (i + 2 >= s.length()) return false;\n            if (!isHex(s.charAt(i+1)) || !isHex(s.charAt(i+2))) return false;\n            i += 2;\n        }\n    }\n    return true;\n}","tryCatchPattern":"try {\n    decoded = URIDecoder.decodeURIComponent(raw);\n} catch (IllegalArgumentException e) {\n    if (e.getMessage() != null && e.getMessage().contains(\"invalid escape sequence\")) {\n    throw new BadRequestException(\"Non-hex percent-escape in: \" + raw);\n    } else throw e;\n}","preventionTips":["Encode user text with encodeURIComponent/java.net.URLEncoder before decoding","Treat any raw '%' in user data as '%25' — sanitize before decode","Reject at the edge: validate query/path params with a strict regex filter","Do not pass template strings (e.g. '%{var}') directly to decoders"],"tags":["uri","percent-encoding","decoding"],"backgroundTag":"malformed-percent-encoding","analyzedSha":"e1c734241f34c7919086ceb4c9262b4a58f6de44","analyzedAt":"2026-09-05T17:01:29.979Z","contentChangedAt":"2026-09-05T17:01:29.979Z","schemaVersion":2},"datasetVersion":"2026-09-12T22:17:10.623Z"}