{"record":{"id":"dab9f51a19d3467a","repo":"quarkusio/quarkus","slug":"partial-escape-sequence-at-end-of-string-s","errorCode":null,"errorMessage":"partial escape sequence at end of string: ${s}","messagePattern":"partial escape sequence at end of string: (.+?)","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":76,"sourceCode":"        if (!modified) {\n            return s;\n        }\n        final byte[] buf = s.getBytes(StandardCharsets.UTF_8);\n        int pos = i; // position in `buf'.\n        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);","sourceCodeStart":58,"sourceCodeEnd":94,"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#L58-L94","documentation":"URIDecoder.decodeURIComponent() requires two hex digits after '%'. When the first character after '%' is not '%' but there are fewer than two remaining characters (i >= size - 1), the escape sequence is incomplete and this IllegalArgumentException is thrown. It prevents decoding a half-specified byte value.","triggerScenarios":"Calling decodeURIComponent with input ending in '%X' (one hex digit), e.g. \"/a%C\" or \"value=%2\" — the percent escape is cut short at the end of the string.","commonSituations":"URLs truncated by length limits or copy/paste; hand-built query strings where a two-digit hex code was typed with only one digit; log lines or substrings cut mid-escape.","solutions":["Provide both hex digits for every escape (e.g. %2F not %2)","Validate input with a regex like /^([^%]|%[0-9A-Fa-f]{2})*$/ before decoding","Re-encode the source string properly (encodeURIComponent / UriBuilder) instead of hand-assembling","Catch IllegalArgumentException and reject the request with 400 Bad Request"],"exampleFix":"// before\nURIDecoder.decodeURIComponent(\"/files/a%2\");\n// after\nURIDecoder.decodeURIComponent(\"/files/a%2F\");","handlingStrategy":"validation","validationCode":"private static final Pattern PCT = Pattern.compile(\"^([^%]|%[0-9A-Fa-f]{2})*$\");\nstatic void requireValidPctEncoding(String s) {\n    if (!PCT.matcher(s).matches()) throw new IllegalArgumentException(\"bad percent-encoding: \" + s);\n}","typeGuard":"static boolean hasCompleteTrailingEscape(String s) {\n    if (s == null || !s.contains(\"%\")) return true;\n    int i = s.lastIndexOf('%');\n    return s.length() - i > 2 && isHex(s.charAt(i + 1)) && isHex(s.charAt(i + 2));\n}","tryCatchPattern":"try {\n    decoded = URIDecoder.decodeURIComponent(raw);\n} catch (IllegalArgumentException e) {\n    if (e.getMessage() != null && e.getMessage().contains(\"partial escape\")) {\n    throw new BadRequestException(\"Incomplete percent-escape in: \" + raw);\n    } else throw e;\n}","preventionTips":["Use two hex digits for every escape (%2F, not %2)","Build URLs with UriBuilder/URLEncoder rather than string concatenation","Check truncation sources (length limits, log cutting) that split escapes","Unit-test decoders with edge-case strings ending in '%X'"],"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-14T00:17:10.932Z"}