From 5519e57baaf23e591d9953feec295e187eeb5774 Mon Sep 17 00:00:00 2001 From: Ashraf Ali Date: Sat, 29 Aug 2026 23:53:18 +0600 Subject: [PATCH] Don't create exceptions in RegexExtractor on failed extractions Every failed extraction called RegexExtractor#removeGroups, which parsed the (still absent) group-count variable with Integer#parseInt. Parsing null always throws a NumberFormatException, which was caught and silently discarded, so each failing extraction paid for creating an exception. In failure-heavy test runs the failed extractions are the hot path, e.g. the reporter of the issue measured thousands of discarded exceptions per second. Parse the internal group-count and match-count variables without relying on exceptions. The parsing behaves like before: absent variables yield the previous default values, and values that are present but not numbers still log a warning. Closes #6240 --- .../jmeter/extractor/RegexExtractor.java | 72 ++++++++++++------- .../jmeter/extractor/TestRegexExtractor.java | 68 ++++++++++++++++++ xdocs/changes.xml | 1 + 3 files changed, 115 insertions(+), 26 deletions(-) diff --git a/src/components/src/main/java/org/apache/jmeter/extractor/RegexExtractor.java b/src/components/src/main/java/org/apache/jmeter/extractor/RegexExtractor.java index 283f7d92b25..2a1b87e3b41 100644 --- a/src/components/src/main/java/org/apache/jmeter/extractor/RegexExtractor.java +++ b/src/components/src/main/java/org/apache/jmeter/extractor/RegexExtractor.java @@ -130,11 +130,7 @@ private void extractWithOroRegex(SampleResult previousResult, JMeterVariables va String prevString = vars.get(refName + REF_MATCH_NR); if (prevString != null) { vars.remove(refName + REF_MATCH_NR);// ensure old value is not left defined - try { - prevCount = Integer.parseInt(prevString); - } catch (NumberFormatException nfe) { - log.warn("Could not parse number: '{}'", prevString); - } + prevCount = parseIntOrDefault(prevString, 0); } int matchCount=0;// Number of refName_n variable sets to keep try { @@ -188,11 +184,7 @@ private void extractWithJavaRegex(SampleResult previousResult, JMeterVariables v String prevString = vars.get(refName + REF_MATCH_NR); if (prevString != null) { vars.remove(refName + REF_MATCH_NR);// ensure old value is not left defined - try { - prevCount = Integer.parseInt(prevString); - } catch (NumberFormatException nfe) { - log.warn("Could not parse number: '{}'", prevString); - } + prevCount = parseIntOrDefault(prevString, 0); } int matchCount=0;// Number of refName_n variable sets to keep try { @@ -355,11 +347,7 @@ private static void saveGroups(JMeterVariables vars, String basename, MatchResul String prevString=vars.get(buf.toString()); int previous=0; if (prevString!=null){ - try { - previous=Integer.parseInt(prevString); - } catch (NumberFormatException nfe) { - log.warn("Could not parse number: '{}'.", prevString); - } + previous=parseIntOrDefault(prevString, 0); } //Note: match.groups() includes group 0 final int groups = match.groups(); @@ -384,11 +372,7 @@ private static void saveGroups(JMeterVariables vars, String basename, java.util. String prevString=vars.get(buf.toString()); int previous=0; if (prevString!=null){ - try { - previous=Integer.parseInt(prevString); - } catch (NumberFormatException nfe) { - log.warn("Could not parse number: '{}'.", prevString); - } + previous=parseIntOrDefault(prevString, 0); } //Note: match.groups() includes group 0, groupCount() not final int groups = match.groupCount() + 1; @@ -410,18 +394,54 @@ private static void saveGroups(JMeterVariables vars, String basename, java.util. * basename_gn, where n=0...# of groups
* basename_g = number of groups (apart from g0) */ + /** + * Parses the given string as a signed decimal integer without relying on + * exceptions, so the hot path of failing extractions does not pay for + * exception creation (see issue #6240). + * + * @param s string to parse, may be null, empty or not a number + * @param defaultValue value to return when the string cannot be parsed + * @return the parsed value, or {@code defaultValue} if the string is null, + * empty, not a signed decimal number, or overflows an {@code int} + */ + private static int parseIntOrDefault(String s, int defaultValue) { + if (s != null && !s.isEmpty() && isSignedDigits(s)) { + try { + return Integer.parseInt(s); + } catch (NumberFormatException overflow) { + // Digit string longer than the int range, fall through to the default + } + } + if (s != null && !s.isEmpty()) { + log.warn("Could not parse number: '{}'", s); + } + return defaultValue; + } + + private static boolean isSignedDigits(String s) { + char first = s.charAt(0); + int start = first == '-' || first == '+' ? 1 : 0; + if (start == s.length()) { + return false; + } + for (int i = start; i < s.length(); i++) { + char c = s.charAt(i); + if (c < '0' || c > '9') { + return false; + } + } + return true; + } + private static void removeGroups(JMeterVariables vars, String basename) { StringBuilder buf = new StringBuilder(); buf.append(basename); buf.append("_g"); // $NON-NLS-1$ int pfxlen=buf.length(); // How many groups are there? - int groups; - try { - groups=Integer.parseInt(vars.get(buf.toString())); - } catch (NumberFormatException e) { - groups=0; - } + // The group-count variable is absent whenever no match has succeeded + // yet, so parsing must cope with null without creating exceptions (#6240) + int groups = parseIntOrDefault(vars.get(buf.toString()), 0); vars.remove(buf.toString());// Remove the group count for (int i = 0; i <= groups; i++) { buf.append(i); diff --git a/src/components/src/test/java/org/apache/jmeter/extractor/TestRegexExtractor.java b/src/components/src/test/java/org/apache/jmeter/extractor/TestRegexExtractor.java index d04385f1a8e..6f278a4ed64 100644 --- a/src/components/src/test/java/org/apache/jmeter/extractor/TestRegexExtractor.java +++ b/src/components/src/test/java/org/apache/jmeter/extractor/TestRegexExtractor.java @@ -479,4 +479,72 @@ public void testScope2() { final String found = vars.get("regVal"); assertTrue(found.equals("ONE") || found.equals("TWO")); } + + // Tests for #6240: failing extractions must not create exceptions while + // cleaning up the group variables + + @Test + public void testNoMatchOnFreshVariablesAppliesDefault() { + extractor.setRegex("nonexistent-(\\d+)"); + extractor.setTemplate("$1$"); + extractor.setDefaultValue("NOTFOUND"); + extractor.setMatchNumber(1); + extractor.process(); + assertEquals("NOTFOUND", vars.get("regVal")); + // No group variables may be left behind + assertNull(vars.get("regVal_g")); + assertNull(vars.get("regVal_g0")); + assertNull(vars.get("regVal_g1")); + assertNull(vars.get("regVal_matchNr")); + } + + @Test + public void testNoMatchCleansUpPreviousGroupVariables() { + extractor.setRegex("RetCode\">(\\w+)<"); + extractor.setTemplate("$1$"); + extractor.setMatchNumber(1); + extractor.process(); + assertEquals("LIS_OK", vars.get("regVal")); + assertEquals("1", vars.get("regVal_g")); + assertEquals("LIS_OK", vars.get("regVal_g1")); + + // Now fail the extraction: previous group variables must be cleaned up + extractor.setRegex("nonexistent-(\\d+)"); + extractor.setDefaultValue("NOTFOUND"); + extractor.process(); + assertEquals("NOTFOUND", vars.get("regVal")); + assertNull(vars.get("regVal_g")); + assertNull(vars.get("regVal_g0")); + assertNull(vars.get("regVal_g1")); + } + + @Test + public void testNoMatchWithTamperedGroupCountVariable() { + vars.put("regVal_g", "not-a-number"); + vars.put("regVal_g0", "stale"); + extractor.setRegex("nonexistent-(\\d+)"); + extractor.setTemplate("$1$"); + extractor.setDefaultValue("NOTFOUND"); + extractor.setMatchNumber(1); + extractor.process(); + assertEquals("NOTFOUND", vars.get("regVal")); + assertNull(vars.get("regVal_g")); + assertNull(vars.get("regVal_g0")); + } + + @Test + public void testAllMatchesWithTamperedMatchNumberVariable() { + vars.put("regVal_matchNr", "not-a-number"); + vars.put("content", "one, two, 3, 45"); + extractor.setRegex("(\\d+)"); + extractor.setTemplate("$1$"); + extractor.setMatchNumber(-1); + extractor.setScopeVariable("content"); + extractor.process(); + // The tampered counter must not break the extraction + assertEquals("2", vars.get("regVal_matchNr")); + assertEquals("3", vars.get("regVal_1")); + assertEquals("45", vars.get("regVal_2")); + assertNull(vars.get("regVal_3")); + } } diff --git a/xdocs/changes.xml b/xdocs/changes.xml index 13e0d097e6d..343cfe69575 100644 --- a/xdocs/changes.xml +++ b/xdocs/changes.xml @@ -109,6 +109,7 @@ Summary Bug fixes

General