diff --git a/CHANGELOG.md b/CHANGELOG.md
index 216894ff..fb33e2aa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
# Change Log
+## [v6.17.1](https://github.com/PerimeterX/perimeterx-java-sdk/compare/v6.17.1...HEAD) (2026-06-29)
+- Fix ClassCastException when adding headers to non-RequestWrapper request
+
+
## [v6.17.0](https://github.com/PerimeterX/perimeterx-java-sdk/compare/v6.17.0...HEAD) (2026-05-24)
- Added Jakarta EE support via new `perimeterx-sdk-jakarta` artifact (Spring Boot 3, Tomcat 10+)
- Fixed first party captcha reverse proxy to use `startsWith` instead of `contains` when matching the captcha prefix
diff --git a/README.md b/README.md
index ec46f2a0..df5c04b5 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
# [PerimeterX](http://www.perimeterx.com) Java SDK
-> Latest stable version: [v6.17.0](https://search.maven.org/#artifactdetails%7Ccom.perimeterx%7Cperimeterx-sdk%7C6.17.0%7Cjar)
+> Latest stable version: [v6.17.1](https://search.maven.org/#artifactdetails%7Ccom.perimeterx%7Cperimeterx-sdk%7C6.17.1%7Cjar)
## Table of Contents
diff --git a/perimeterx-sdk-jakarta/pom.xml b/perimeterx-sdk-jakarta/pom.xml
index 76dac18e..93fde185 100644
--- a/perimeterx-sdk-jakarta/pom.xml
+++ b/perimeterx-sdk-jakarta/pom.xml
@@ -8,7 +8,7 @@
com.perimeterx
perimeterx-java-sdk-parent
- 6.17.0
+ 6.17.1
../pom.xml
diff --git a/perimeterx-sdk/pom.xml b/perimeterx-sdk/pom.xml
index 1d4f78a2..ecc00c9a 100644
--- a/perimeterx-sdk/pom.xml
+++ b/perimeterx-sdk/pom.xml
@@ -8,7 +8,7 @@
com.perimeterx
perimeterx-java-sdk-parent
- 6.17.0
+ 6.17.1
../pom.xml
diff --git a/perimeterx-sdk/src/main/java/com/perimeterx/api/PerimeterX.java b/perimeterx-sdk/src/main/java/com/perimeterx/api/PerimeterX.java
index 42d7bd85..35b669f2 100644
--- a/perimeterx-sdk/src/main/java/com/perimeterx/api/PerimeterX.java
+++ b/perimeterx-sdk/src/main/java/com/perimeterx/api/PerimeterX.java
@@ -242,9 +242,17 @@ private void addCustomHeadersToRequest(HttpServletRequest request, PXContext con
setDataEnrichmentHeader(request, context);
}
+ private void addHeaderSafely(HttpServletRequest request, String headerName, String headerValue, PXContext context) {
+ if (request instanceof RequestWrapper) {
+ ((RequestWrapper) request).addHeader(headerName, headerValue);
+ } else {
+ request.setAttribute(headerName, headerValue);
+ }
+ }
+
private void setBreachedAccount(HttpServletRequest request, PXContext context) {
if (configuration.isLoginCredentialsExtractionEnabled() && context.isBreachedAccount()) {
- ((RequestWrapper) request).addHeader(configuration.getPxCompromisedCredentialsHeader(), String.valueOf(context.getPxde().get(BREACHED_ACCOUNT_KEY_NAME)));
+ addHeaderSafely(request, configuration.getPxCompromisedCredentialsHeader(), String.valueOf(context.getPxde().get(BREACHED_ACCOUNT_KEY_NAME)), context);
}
}
@@ -255,8 +263,8 @@ private void setAdditionalS2SActivityHeaders(HttpServletRequest request, PXConte
final String stringifyActivity = new Gson().toJson(activity);
final String urlHeader = configuration.getServerURL() + API_ACTIVITIES;
- ((RequestWrapper) request).addHeader(ADDITIONAL_ACTIVITY_HEADER, stringifyActivity);
- ((RequestWrapper) request).addHeader(ADDITIONAL_ACTIVITY_URL_HEADER, urlHeader);
+ addHeaderSafely(request, ADDITIONAL_ACTIVITY_HEADER, stringifyActivity, context);
+ addHeaderSafely(request, ADDITIONAL_ACTIVITY_URL_HEADER, urlHeader, context);
}
}
@@ -274,7 +282,7 @@ private void setDataEnrichmentHeader(HttpServletRequest request, PXContext conte
String pxdeJson = context.getPxde().toString();
byte[] utf8Bytes = pxdeJson.getBytes(StandardCharsets.UTF_8);
String encodedPxde = new String(utf8Bytes, StandardCharsets.ISO_8859_1);
- ((RequestWrapper) request).addHeader(headerName, encodedPxde);
+ addHeaderSafely(request, headerName, encodedPxde, context);
} catch (Exception e) {
context.logger.debug("Failed to add data enrichment header", e);
}
diff --git a/perimeterx-sdk/src/test/java/com/perimeterx/api/DataEnrichmentHeaderTest.java b/perimeterx-sdk/src/test/java/com/perimeterx/api/DataEnrichmentHeaderTest.java
new file mode 100644
index 00000000..1dedfd6c
--- /dev/null
+++ b/perimeterx-sdk/src/test/java/com/perimeterx/api/DataEnrichmentHeaderTest.java
@@ -0,0 +1,142 @@
+package com.perimeterx.api;
+
+import com.perimeterx.http.PXClient;
+import com.perimeterx.http.RequestWrapper;
+import com.perimeterx.models.PXContext;
+import com.perimeterx.models.configuration.ModuleMode;
+import com.perimeterx.models.configuration.PXConfiguration;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+import testutils.TestObjectUtils;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponseWrapper;
+
+@Test
+public class DataEnrichmentHeaderTest {
+
+ private static final String DE_HEADER_NAME = "X-PX-Data-Enrichment";
+
+ private PXConfiguration configuration;
+
+ @BeforeMethod
+ public void setup() {
+ configuration = PXConfiguration.builder()
+ .appId("appId")
+ .authToken("token")
+ .cookieKey("cookieKey")
+ .moduleMode(ModuleMode.BLOCKING)
+ .remoteConfigurationEnabled(false)
+ .blockingScore(70)
+ .pxDataEnrichmentHeaderName(DE_HEADER_NAME)
+ .build();
+ }
+
+ /**
+ * Simulates the customer's scenario: a custom HttpServletRequestWrapper
+ * that is NOT our RequestWrapper. Before the fix, this would throw a
+ * ClassCastException inside setDataEnrichmentHeader.
+ */
+ @Test
+ public void testNonRequestWrapper_oldBehaviorWouldThrowClassCastException() {
+ MockHttpServletRequest mockRequest = new MockHttpServletRequest();
+ HttpServletRequest customWrapper = new HttpServletRequestWrapper(mockRequest);
+
+ Assert.assertFalse(customWrapper instanceof RequestWrapper,
+ "Test precondition: the custom wrapper must NOT be a RequestWrapper");
+
+ boolean wouldThrow = false;
+ try {
+ ((RequestWrapper) customWrapper).addHeader(DE_HEADER_NAME, "test");
+ } catch (ClassCastException e) {
+ wouldThrow = true;
+ }
+ Assert.assertTrue(wouldThrow,
+ "Direct cast to RequestWrapper should throw ClassCastException for non-RequestWrapper requests");
+ }
+
+ /**
+ * With the fix: passing a non-RequestWrapper request to pxVerify should
+ * NOT throw, and the DE data should be available via request.getAttribute().
+ */
+ @Test
+ public void testNonRequestWrapper_dataEnrichmentSetAsAttribute() throws Exception {
+ PXClient client = TestObjectUtils.nonBlockingPXClient(configuration.getBlockingScore());
+ PerimeterX perimeterx = TestObjectUtils.testablePerimeterXObject(configuration, client);
+
+ MockHttpServletRequest mockRequest = new MockHttpServletRequest();
+ HttpServletRequest customWrapper = new HttpServletRequestWrapper(mockRequest);
+
+ Assert.assertFalse(customWrapper instanceof RequestWrapper,
+ "Test precondition: must not be a RequestWrapper");
+
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ PXContext context = perimeterx.pxVerify(customWrapper, new HttpServletResponseWrapper(response));
+
+ Assert.assertNotNull(context, "pxVerify should not return null");
+ Assert.assertNotEquals(response.getStatus(), 403, "Request should not be blocked");
+
+ String deValue = (String) customWrapper.getAttribute(DE_HEADER_NAME);
+ Assert.assertNotNull(deValue,
+ "Data enrichment should be available as a request attribute when request is not a RequestWrapper");
+ Assert.assertTrue(deValue.contains("cookieMonster"),
+ "Data enrichment attribute should contain the expected PXDE data");
+ }
+
+ /**
+ * With RequestWrapper: existing behavior is preserved.
+ * The DE data should be available via request.getHeader().
+ */
+ @Test
+ public void testRequestWrapper_dataEnrichmentSetAsHeader() throws Exception {
+ PXClient client = TestObjectUtils.nonBlockingPXClient(configuration.getBlockingScore());
+ PerimeterX perimeterx = TestObjectUtils.testablePerimeterXObject(configuration, client);
+
+ MockHttpServletRequest mockRequest = new MockHttpServletRequest();
+ RequestWrapper wrappedRequest = new RequestWrapper(mockRequest);
+
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ PXContext context = perimeterx.pxVerify(wrappedRequest, new HttpServletResponseWrapper(response));
+
+ Assert.assertNotNull(context, "pxVerify should not return null");
+ Assert.assertNotEquals(response.getStatus(), 403, "Request should not be blocked");
+
+ String deHeader = wrappedRequest.getHeader(DE_HEADER_NAME);
+ Assert.assertNotNull(deHeader,
+ "Data enrichment should be available as a request header when using RequestWrapper");
+ Assert.assertTrue(deHeader.contains("cookieMonster"),
+ "Data enrichment header should contain the expected PXDE data");
+ }
+
+ /**
+ * When DE header is not configured, neither header nor attribute should be set,
+ * regardless of request type.
+ */
+ @Test
+ public void testNoHeaderConfigured_nothingSet() throws Exception {
+ PXConfiguration noDeConfig = PXConfiguration.builder()
+ .appId("appId")
+ .authToken("token")
+ .cookieKey("cookieKey")
+ .moduleMode(ModuleMode.BLOCKING)
+ .remoteConfigurationEnabled(false)
+ .blockingScore(70)
+ .build();
+
+ PXClient client = TestObjectUtils.nonBlockingPXClient(noDeConfig.getBlockingScore());
+ PerimeterX perimeterx = TestObjectUtils.testablePerimeterXObject(noDeConfig, client);
+
+ MockHttpServletRequest mockRequest = new MockHttpServletRequest();
+ HttpServletRequest customWrapper = new HttpServletRequestWrapper(mockRequest);
+
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ perimeterx.pxVerify(customWrapper, new HttpServletResponseWrapper(response));
+
+ Assert.assertNull(customWrapper.getAttribute(DE_HEADER_NAME),
+ "No attribute should be set when DE header name is not configured");
+ }
+}
diff --git a/pom.xml b/pom.xml
index 4cfcceb1..11dedb50 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
4.0.0
com.perimeterx
perimeterx-java-sdk-parent
- 6.17.0
+ 6.17.1
pom
PerimeterX Java SDK Parent
diff --git a/px_metadata.json b/px_metadata.json
index 9c94afdb..f3b282f6 100644
--- a/px_metadata.json
+++ b/px_metadata.json
@@ -1,5 +1,5 @@
{
- "version": "6.17.0",
+ "version": "6.17.1",
"supported_features": [
"advanced_blocking_response",
"batched_activities",
diff --git a/web-jakarta/pom.xml b/web-jakarta/pom.xml
index ccfa5071..423d5a22 100644
--- a/web-jakarta/pom.xml
+++ b/web-jakarta/pom.xml
@@ -12,7 +12,7 @@
17
- 6.17.0
+ 6.17.1
diff --git a/web/pom.xml b/web/pom.xml
index 9ff1d9ef..0cb72217 100644
--- a/web/pom.xml
+++ b/web/pom.xml
@@ -13,7 +13,7 @@
8
8
- 6.17.0
+ 6.17.1