From 4354737975acfabe07eadd4c0ef06d9eb6ffdfbe Mon Sep 17 00:00:00 2001 From: Joaquin Araujo Date: Mon, 8 Jun 2026 22:20:16 -0300 Subject: [PATCH] Wrap exception as sample result error instead of breaking JMeter thread Prevent the exception like URI is not absolute due to the resolution of a malformed URI to be reported all the way up to Jmeter causing thread to be stopped. Platforms like Blazemeter often stop test execution due to errors like this. --- .../core/VideoStreamingHttpClient.java | 27 ++++++++- .../core/VideoStreamingSampler.java | 9 +++ .../core/VideoStreamingHttpClientTest.java | 57 +++++++++++++++++++ .../videostreaming/hls/HlsSamplerTest.java | 12 ++++ 4 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/blazemeter/jmeter/videostreaming/core/VideoStreamingHttpClientTest.java diff --git a/src/main/java/com/blazemeter/jmeter/videostreaming/core/VideoStreamingHttpClient.java b/src/main/java/com/blazemeter/jmeter/videostreaming/core/VideoStreamingHttpClient.java index e795a6c..f4b2939 100644 --- a/src/main/java/com/blazemeter/jmeter/videostreaming/core/VideoStreamingHttpClient.java +++ b/src/main/java/com/blazemeter/jmeter/videostreaming/core/VideoStreamingHttpClient.java @@ -19,6 +19,9 @@ */ public class VideoStreamingHttpClient extends HTTPHC4Impl { + private static final String NON_HTTP_RESPONSE_CODE = "Non HTTP response code"; + private static final String NON_HTTP_RESPONSE_MESSAGE = "Non HTTP response message"; + private transient volatile boolean interrupted = false; private Map headers = new HashMap<>(); @@ -53,13 +56,35 @@ public HTTPSampleResult downloadUri(URI uri) { if (interrupted) { throw new SamplerInterruptedException(); } + if (uri == null || !uri.isAbsolute()) { + return buildInvalidUriResult(uri, "URI is not absolute"); + } try { return sample(uri.toURL(), "GET", false, 0); } catch (MalformedURLException e) { - throw new IllegalArgumentException(e); + return buildInvalidUriResult(uri, e.getMessage()); } } + /* + When the resolved URI can't be turned into an absolute URL (e.g. a dynamic master URL + variable resolved to an empty value or a relative path), we return a failed sample result + instead of throwing an uncaught IllegalArgumentException. This way the failure is recorded as + a measurable sample and handled by the normal download error path, rather than aborting the + sampler with a raw stack trace in the logs. + */ + private HTTPSampleResult buildInvalidUriResult(URI uri, String detail) { + HTTPSampleResult result = new HTTPSampleResult(); + result.sampleStart(); + result.sampleEnd(); + result.setSuccessful(false); + result.setResponseCode( + NON_HTTP_RESPONSE_CODE + ": " + IllegalArgumentException.class.getName()); + result.setResponseMessage(NON_HTTP_RESPONSE_MESSAGE + ": " + + detail + " [" + (uri != null ? uri : "null") + "]"); + return result; + } + public void addHeader(String name, String value) { headers.put(name, value); } diff --git a/src/main/java/com/blazemeter/jmeter/videostreaming/core/VideoStreamingSampler.java b/src/main/java/com/blazemeter/jmeter/videostreaming/core/VideoStreamingSampler.java index d278e49..88eaed2 100644 --- a/src/main/java/com/blazemeter/jmeter/videostreaming/core/VideoStreamingSampler.java +++ b/src/main/java/com/blazemeter/jmeter/videostreaming/core/VideoStreamingSampler.java @@ -82,6 +82,15 @@ public SampleResult sample() { sample(masterUri, baseSampler.getBandwidthSelector(), baseSampler.getResolutionSelector(), baseSampler.getAudioLanguage(), baseSampler.getSubtitleLanguage(), baseSampler.getPlaySecondsOrWarn()); + } catch (IllegalArgumentException e) { + // the master URL (often a dynamic variable) is not a valid URI reference; record a failed + // sample instead of letting the exception abort the sampler with a raw stack trace + LOG.warn("Invalid master playlist URL '{}'", baseSampler.getMasterUrl(), e); + HTTPSampleResult result = new HTTPSampleResult(); + result.sampleStart(); + result.sampleEnd(); + sampleResultProcessor.accept(buildPlaylistName(MASTER_TYPE_NAME), + baseSampler.errorResult(result, e)); } catch (SamplerInterruptedException e) { LOG.debug("Sampler interrupted by JMeter", e); } catch (InterruptedException e) { diff --git a/src/test/java/com/blazemeter/jmeter/videostreaming/core/VideoStreamingHttpClientTest.java b/src/test/java/com/blazemeter/jmeter/videostreaming/core/VideoStreamingHttpClientTest.java new file mode 100644 index 0000000..d7689e6 --- /dev/null +++ b/src/test/java/com/blazemeter/jmeter/videostreaming/core/VideoStreamingHttpClientTest.java @@ -0,0 +1,57 @@ +package com.blazemeter.jmeter.videostreaming.core; + +import static org.assertj.core.api.Java6Assertions.assertThat; + +import com.blazemeter.jmeter.JMeterTestUtils; +import com.blazemeter.jmeter.hls.logic.HlsSampler; +import java.net.URI; +import org.apache.jmeter.protocol.http.sampler.HTTPSampleResult; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +public class VideoStreamingHttpClientTest { + + private VideoStreamingHttpClient httpClient; + + @BeforeClass + public static void setupClass() { + JMeterTestUtils.setupJmeterEnv(); + } + + @Before + public void setup() { + httpClient = new VideoStreamingHttpClient(new HlsSampler()); + } + + /* + Reproduces the production scenario where a dynamic master URL variable resolves to a value + without a scheme (e.g. the JSON extractor default "C_hlsManifestPath_Not_Found"). Such a value + is a valid relative URI, so URI.create succeeds but URI.toURL() throws "URI is not absolute". + The client must not propagate that exception; it must return a failed sample result. + */ + @Test + public void shouldReturnFailedResultWhenUriIsNotAbsolute() { + HTTPSampleResult result = httpClient.downloadUri(URI.create("C_hlsManifestPath_Not_Found")); + assertFailedResult(result); + } + + @Test + public void shouldReturnFailedResultWhenUriIsRelativePath() { + HTTPSampleResult result = httpClient.downloadUri(URI.create("/relative/master.m3u8")); + assertFailedResult(result); + } + + @Test + public void shouldReturnFailedResultWhenUriIsNull() { + HTTPSampleResult result = httpClient.downloadUri(null); + assertFailedResult(result); + } + + private void assertFailedResult(HTTPSampleResult result) { + assertThat(result).isNotNull(); + assertThat(result.isSuccessful()).isFalse(); + assertThat(result.getResponseCode()).contains(IllegalArgumentException.class.getName()); + } + +} diff --git a/src/test/java/com/blazemeter/jmeter/videostreaming/hls/HlsSamplerTest.java b/src/test/java/com/blazemeter/jmeter/videostreaming/hls/HlsSamplerTest.java index 3d69b1f..f0b9246 100644 --- a/src/test/java/com/blazemeter/jmeter/videostreaming/hls/HlsSamplerTest.java +++ b/src/test/java/com/blazemeter/jmeter/videostreaming/hls/HlsSamplerTest.java @@ -953,4 +953,16 @@ public void shouldFallbackToAtLeastOneSegmentWhenTargetDurationMissing() throws buildMediaSegmentSampleResult(5)); } + @Test + public void shouldRecordFailedMasterSampleWhenMasterUrlIsNotValidUriReference() { + // unresolved dynamic variable (contains illegal URI characters) so URI.create itself fails + baseSampler.setMasterUrl("${C_hlsManifestPath}"); + sampler.sample(); + ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(SampleResult.class); + verify(sampleResultProcessor).accept(nameCaptor.capture(), resultCaptor.capture()); + assertThat(nameCaptor.getValue()).isEqualTo("master playlist"); + assertThat(resultCaptor.getValue().isSuccessful()).isFalse(); + } + }