From 29f33a29e9c5547d78736b2fda56dc6073d95e9d Mon Sep 17 00:00:00 2001 From: Caroline6312 Date: Mon, 5 Jan 2026 12:54:44 -0800 Subject: [PATCH 1/8] Handle multipart request errors in optout service to return 400 instead of 500 --- .../optout/vertx/GenericFailureHandler.java | 74 +++++++++++++++++++ .../optout/vertx/OptOutServiceVerticle.java | 2 + 2 files changed, 76 insertions(+) create mode 100644 src/main/java/com/uid2/optout/vertx/GenericFailureHandler.java diff --git a/src/main/java/com/uid2/optout/vertx/GenericFailureHandler.java b/src/main/java/com/uid2/optout/vertx/GenericFailureHandler.java new file mode 100644 index 0000000..cdf2eec --- /dev/null +++ b/src/main/java/com/uid2/optout/vertx/GenericFailureHandler.java @@ -0,0 +1,74 @@ +package com.uid2.optout.vertx; + +import io.vertx.core.Handler; +import io.vertx.core.http.HttpClosedException; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.ext.web.RoutingContext; +import org.apache.http.impl.EnglishReasonPhraseCatalog; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class GenericFailureHandler implements Handler { + private static final Logger LOGGER = LoggerFactory.getLogger(GenericFailureHandler.class); + + @Override + public void handle(RoutingContext ctx) { + int statusCode = ctx.statusCode(); + HttpServerResponse response = ctx.response(); + String url = ctx.normalizedPath(); + Throwable t = ctx.failure(); + + String errorMsg = t.getMessage(); + String className = t.getClass().getName(); + + // Handle multipart method mismatch (IllegalStateException) + if (t instanceof IllegalStateException && + errorMsg != null && + errorMsg.equalsIgnoreCase( + "Request method must be one of POST, PUT, PATCH or DELETE to decode a multipart request")) { + if (!response.ended() && !response.closed()) { + response.setStatusCode(400) + .end("Bad Request: multipart content not allowed with this HTTP method"); + } + LOGGER.warn("URL: [{}] - Multipart method mismatch - Error:", url, t); + return; + } + + // Handle TooManyFormFieldsException + if (className.contains("TooManyFormFieldsException")) { + if (!response.ended() && !response.closed()) { + response.setStatusCode(400) + .end("Bad Request: Too many form fields"); + } + LOGGER.warn("URL: [{}] - Too many form fields - Error:", url, t); + return; + } + + // Handle HttpClosedException - ignore as it's usually caused by users and has no impact + if (t instanceof HttpClosedException) { + LOGGER.warn("Ignoring exception - URL: [{}] - Error:", url, t); + if (!response.ended() && !response.closed()) { + response.end(); + } + return; + } + + // Handle other exceptions based on status code + // If no status code was set, default to 500 + int finalStatusCode = statusCode == -1 ? 500 : statusCode; + + if (finalStatusCode >= 500 && finalStatusCode < 600) { // 5xx is server error, so error + LOGGER.error("URL: [{}] - Error response code: [{}] - Error:", url, finalStatusCode, t); + } else if (finalStatusCode >= 400 && finalStatusCode < 500) { // 4xx is user error, so just warn + LOGGER.warn("URL: [{}] - Error response code: [{}] - Error:", url, finalStatusCode, t); + } else { + // Status code not in 4xx or 5xx range, log as error + LOGGER.error("URL: [{}] - Unexpected status code: [{}] - Error:", url, finalStatusCode, t); + } + + if (!response.ended() && !response.closed()) { + response.setStatusCode(finalStatusCode) + .end(EnglishReasonPhraseCatalog.INSTANCE.getReason(finalStatusCode, null)); + } + } +} diff --git a/src/main/java/com/uid2/optout/vertx/OptOutServiceVerticle.java b/src/main/java/com/uid2/optout/vertx/OptOutServiceVerticle.java index 5197ee4..e652b7c 100644 --- a/src/main/java/com/uid2/optout/vertx/OptOutServiceVerticle.java +++ b/src/main/java/com/uid2/optout/vertx/OptOutServiceVerticle.java @@ -231,6 +231,8 @@ private Router createRouter() { //// if enabled, this would add handler for exposing prometheus metrics // router.route("/metrics").handler(PrometheusScrapingHandler.create()); + router.route().failureHandler(new GenericFailureHandler()); + return router; } From 3bc4a861d032f72f3cab6280b395e699705f0674 Mon Sep 17 00:00:00 2001 From: Caroline6312 Date: Thu, 8 Jan 2026 12:18:45 -0800 Subject: [PATCH 2/8] Add general failure handler --- .../optout/vertx/GenericFailureHandler.java | 3 - .../vertx/GenericFailureHandlerTest.java | 353 ++++++++++++++++++ 2 files changed, 353 insertions(+), 3 deletions(-) create mode 100644 src/test/java/com/uid2/optout/vertx/GenericFailureHandlerTest.java diff --git a/src/main/java/com/uid2/optout/vertx/GenericFailureHandler.java b/src/main/java/com/uid2/optout/vertx/GenericFailureHandler.java index cdf2eec..8bb61ee 100644 --- a/src/main/java/com/uid2/optout/vertx/GenericFailureHandler.java +++ b/src/main/java/com/uid2/optout/vertx/GenericFailureHandler.java @@ -61,9 +61,6 @@ public void handle(RoutingContext ctx) { LOGGER.error("URL: [{}] - Error response code: [{}] - Error:", url, finalStatusCode, t); } else if (finalStatusCode >= 400 && finalStatusCode < 500) { // 4xx is user error, so just warn LOGGER.warn("URL: [{}] - Error response code: [{}] - Error:", url, finalStatusCode, t); - } else { - // Status code not in 4xx or 5xx range, log as error - LOGGER.error("URL: [{}] - Unexpected status code: [{}] - Error:", url, finalStatusCode, t); } if (!response.ended() && !response.closed()) { diff --git a/src/test/java/com/uid2/optout/vertx/GenericFailureHandlerTest.java b/src/test/java/com/uid2/optout/vertx/GenericFailureHandlerTest.java new file mode 100644 index 0000000..4d4c469 --- /dev/null +++ b/src/test/java/com/uid2/optout/vertx/GenericFailureHandlerTest.java @@ -0,0 +1,353 @@ +package com.uid2.optout.vertx; + +import io.vertx.core.http.HttpClosedException; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.ext.web.RoutingContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class GenericFailureHandlerTest { + @Mock + private RoutingContext routingContext; + @Mock + private HttpServerResponse response; + + private GenericFailureHandler handler; + + @BeforeEach + public void setup() { + handler = new GenericFailureHandler(); + when(routingContext.response()).thenReturn(response); + when(routingContext.normalizedPath()).thenReturn("/test/path"); + when(response.ended()).thenReturn(false); + when(response.closed()).thenReturn(false); + // Mock setStatusCode to return response for method chaining + when(response.setStatusCode(anyInt())).thenReturn(response); + } + + @Test + public void testMultipartMethodMismatch_returns400() { + IllegalStateException exception = new IllegalStateException( + "Request method must be one of POST, PUT, PATCH or DELETE to decode a multipart request"); + + when(routingContext.statusCode()).thenReturn(-1); + when(routingContext.failure()).thenReturn(exception); + + handler.handle(routingContext); + + ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor bodyCaptor = ArgumentCaptor.forClass(String.class); + + verify(response).setStatusCode(statusCaptor.capture()); + verify(response).end(bodyCaptor.capture()); + + assertEquals(400, statusCaptor.getValue()); + assertEquals("Bad Request: multipart content not allowed with this HTTP method", bodyCaptor.getValue()); + } + + @Test + public void testMultipartMethodMismatch_caseInsensitive() { + IllegalStateException exception = new IllegalStateException( + "REQUEST METHOD MUST BE ONE OF POST, PUT, PATCH OR DELETE TO DECODE A MULTIPART REQUEST"); + + when(routingContext.statusCode()).thenReturn(-1); + when(routingContext.failure()).thenReturn(exception); + + handler.handle(routingContext); + + ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Integer.class); + verify(response).setStatusCode(statusCaptor.capture()); + assertEquals(400, statusCaptor.getValue()); + } + + @Test + public void testIllegalStateException_withDifferentMessage_handledNormally() { + IllegalStateException exception = new IllegalStateException("Different error message"); + + when(routingContext.statusCode()).thenReturn(500); + when(routingContext.failure()).thenReturn(exception); + + handler.handle(routingContext); + + // Should not return 400, but use the status code from context + ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Integer.class); + verify(response).setStatusCode(statusCaptor.capture()); + assertEquals(500, statusCaptor.getValue()); + } + + @Test + public void testMultipartMethodMismatch_withNullMessage_handledNormally() { + IllegalStateException exception = new IllegalStateException((String) null); + + when(routingContext.statusCode()).thenReturn(500); + when(routingContext.failure()).thenReturn(exception); + + handler.handle(routingContext); + + // Should not return 400, but use the status code from context + ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Integer.class); + verify(response).setStatusCode(statusCaptor.capture()); + assertEquals(500, statusCaptor.getValue()); + } + + @Test + public void testTooManyFormFieldsException_withNettyClassNamePattern_returns400() { + // Test with an exception that has a class name matching the Netty pattern + // Actual Netty exception: io.netty.handler.codec.http.multipart.HttpPostRequestDecoder$TooManyFormFieldsException + // The handler checks if className.contains("TooManyFormFieldsException"), so this should work + NettyTooManyFormFieldsException exception = new NettyTooManyFormFieldsException(); + + when(routingContext.statusCode()).thenReturn(-1); + when(routingContext.failure()).thenReturn(exception); + + handler.handle(routingContext); + + ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor bodyCaptor = ArgumentCaptor.forClass(String.class); + + verify(response).setStatusCode(statusCaptor.capture()); + verify(response).end(bodyCaptor.capture()); + + assertEquals(400, statusCaptor.getValue()); + assertEquals("Bad Request: Too many form fields", bodyCaptor.getValue()); + + // Verify the class name contains "TooManyFormFieldsException" (matching handler logic) + assertTrue(exception.getClass().getName().contains("TooManyFormFieldsException")); + } + + @Test + public void testHttpClosedException_ignored() { + HttpClosedException exception = new HttpClosedException("Connection closed"); + + when(routingContext.statusCode()).thenReturn(-1); + when(routingContext.failure()).thenReturn(exception); + + handler.handle(routingContext); + + // Should only call end() without setting status code + verify(response, never()).setStatusCode(anyInt()); + verify(response).end(); + } + + @Test + public void testHttpClosedException_responseAlreadyEnded_doesNotCallEnd() { + HttpClosedException exception = new HttpClosedException("Connection closed"); + + when(routingContext.statusCode()).thenReturn(-1); + when(routingContext.failure()).thenReturn(exception); + when(response.ended()).thenReturn(true); + + handler.handle(routingContext); + + verify(response, never()).setStatusCode(anyInt()); + verify(response, never()).end(); + } + + @Test + public void testHttpClosedException_responseClosed_doesNotCallEnd() { + HttpClosedException exception = new HttpClosedException("Connection closed"); + + when(routingContext.statusCode()).thenReturn(-1); + when(routingContext.failure()).thenReturn(exception); + when(response.closed()).thenReturn(true); + + handler.handle(routingContext); + + verify(response, never()).setStatusCode(anyInt()); + verify(response, never()).end(); + } + + @Test + public void test500StatusCode_logsError() { + RuntimeException exception = new RuntimeException("Internal server error"); + + when(routingContext.statusCode()).thenReturn(500); + when(routingContext.failure()).thenReturn(exception); + + handler.handle(routingContext); + + ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Integer.class); + verify(response).setStatusCode(statusCaptor.capture()); + assertEquals(500, statusCaptor.getValue()); + verify(response).end(anyString()); + } + + @Test + public void test503StatusCode_logsError() { + RuntimeException exception = new RuntimeException("Service unavailable"); + + when(routingContext.statusCode()).thenReturn(503); + when(routingContext.failure()).thenReturn(exception); + + handler.handle(routingContext); + + ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Integer.class); + verify(response).setStatusCode(statusCaptor.capture()); + assertEquals(503, statusCaptor.getValue()); + verify(response).end(anyString()); + } + + @Test + public void test400StatusCode_logsWarning() { + IllegalArgumentException exception = new IllegalArgumentException("Bad request"); + + when(routingContext.statusCode()).thenReturn(400); + when(routingContext.failure()).thenReturn(exception); + + handler.handle(routingContext); + + ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Integer.class); + verify(response).setStatusCode(statusCaptor.capture()); + assertEquals(400, statusCaptor.getValue()); + verify(response).end(anyString()); + } + + @Test + public void test404StatusCode_logsWarning() { + RuntimeException exception = new RuntimeException("Not found"); + + when(routingContext.statusCode()).thenReturn(404); + when(routingContext.failure()).thenReturn(exception); + + handler.handle(routingContext); + + ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Integer.class); + verify(response).setStatusCode(statusCaptor.capture()); + assertEquals(404, statusCaptor.getValue()); + verify(response).end(anyString()); + } + + @Test + public void testNoStatusCode_defaultsTo500() { + RuntimeException exception = new RuntimeException("Unknown error"); + + when(routingContext.statusCode()).thenReturn(-1); + when(routingContext.failure()).thenReturn(exception); + + handler.handle(routingContext); + + ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Integer.class); + verify(response).setStatusCode(statusCaptor.capture()); + assertEquals(500, statusCaptor.getValue()); + verify(response).end(anyString()); + } + + @Test + public void testSuccessStatus_status200_responseAlreadyEnded() { + // Sucess status case: Status code is 200 (success) and response is already ended + // The handler should not modify anything since the response is already complete + RuntimeException exception = new RuntimeException("Exception but response already sent"); + + when(routingContext.statusCode()).thenReturn(200); + when(routingContext.failure()).thenReturn(exception); + when(response.ended()).thenReturn(true); // Response already ended - happy case + + handler.handle(routingContext); + + // Handler should not modify the response since it's already ended + verify(response, never()).setStatusCode(anyInt()); + verify(response, never()).end(); + verify(response, never()).end(anyString()); + } + + @Test + public void testResponseAlreadyEnded_doesNotSetStatusCode() { + RuntimeException exception = new RuntimeException("Error"); + + when(routingContext.statusCode()).thenReturn(500); + when(routingContext.failure()).thenReturn(exception); + when(response.ended()).thenReturn(true); + + handler.handle(routingContext); + + verify(response, never()).setStatusCode(anyInt()); + verify(response, never()).end(); + } + + @Test + public void testResponseClosed_doesNotSetStatusCode() { + RuntimeException exception = new RuntimeException("Error"); + + when(routingContext.statusCode()).thenReturn(500); + when(routingContext.failure()).thenReturn(exception); + when(response.closed()).thenReturn(true); + + handler.handle(routingContext); + + verify(response, never()).setStatusCode(anyInt()); + verify(response, never()).end(); + } + + @Test + public void testExceptionWithNullMessage() { + // RuntimeException with no message will have getMessage() return null + RuntimeException exception = new RuntimeException((String) null); + + when(routingContext.statusCode()).thenReturn(500); + when(routingContext.failure()).thenReturn(exception); + + handler.handle(routingContext); + + ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Integer.class); + verify(response).setStatusCode(statusCaptor.capture()); + assertEquals(500, statusCaptor.getValue()); + verify(response).end(anyString()); + } + + @Test + public void testMultipartMethodMismatch_responseAlreadyEnded_doesNotCallEnd() { + IllegalStateException exception = new IllegalStateException( + "Request method must be one of POST, PUT, PATCH or DELETE to decode a multipart request"); + + when(routingContext.statusCode()).thenReturn(-1); + when(routingContext.failure()).thenReturn(exception); + when(response.ended()).thenReturn(true); + + handler.handle(routingContext); + + verify(response, never()).setStatusCode(anyInt()); + verify(response, never()).end(); + } + + @Test + public void testTooManyFormFieldsException_responseAlreadyEnded_doesNotCallEnd() { + TestTooManyFormFieldsException exception = new TestTooManyFormFieldsException("Too many form fields"); + + when(routingContext.statusCode()).thenReturn(-1); + when(routingContext.failure()).thenReturn(exception); + when(response.ended()).thenReturn(true); + + handler.handle(routingContext); + + verify(response, never()).setStatusCode(anyInt()); + verify(response, never()).end(); + } + + // Helper class to simulate TooManyFormFieldsException + // The class name must contain "TooManyFormFieldsException" for the handler to recognize it + private static class TestTooManyFormFieldsException extends RuntimeException { + public TestTooManyFormFieldsException(String message) { + super(message); + } + } + + // Helper class to simulate the actual Netty exception class name pattern + // This matches: io.netty.handler.codec.http.multipart.HttpPostRequestDecoder$TooManyFormFieldsException + // The $ indicates it's an inner class in Netty + private static class NettyTooManyFormFieldsException extends RuntimeException { + public NettyTooManyFormFieldsException() { + super(); + } + } +} From e760df21cd2800bf4c840b2a066cb94b875e8946 Mon Sep 17 00:00:00 2001 From: Caroline6312 Date: Thu, 8 Jan 2026 12:33:50 -0800 Subject: [PATCH 3/8] Fix tests --- .../uid2/optout/vertx/GenericFailureHandler.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/main/java/com/uid2/optout/vertx/GenericFailureHandler.java b/src/main/java/com/uid2/optout/vertx/GenericFailureHandler.java index 8bb61ee..61cfe4c 100644 --- a/src/main/java/com/uid2/optout/vertx/GenericFailureHandler.java +++ b/src/main/java/com/uid2/optout/vertx/GenericFailureHandler.java @@ -18,6 +18,22 @@ public void handle(RoutingContext ctx) { String url = ctx.normalizedPath(); Throwable t = ctx.failure(); + // Handle case where failure() is null (e.g., when rc.fail(401) is called without exception) + // In this case, just use the status code that was set + if (t == null) { + int finalStatusCode = statusCode == -1 ? 500 : statusCode; + if (finalStatusCode >= 500 && finalStatusCode < 600) { + LOGGER.error("URL: [{}] - Error response code: [{}]", url, finalStatusCode); + } else if (finalStatusCode >= 400 && finalStatusCode < 500) { + LOGGER.warn("URL: [{}] - Error response code: [{}]", url, finalStatusCode); + } + if (!response.ended() && !response.closed()) { + response.setStatusCode(finalStatusCode) + .end(EnglishReasonPhraseCatalog.INSTANCE.getReason(finalStatusCode, null)); + } + return; + } + String errorMsg = t.getMessage(); String className = t.getClass().getName(); From c768b003b91d685c8f2c92c086c8a45b30f57986 Mon Sep 17 00:00:00 2001 From: Release Workflow Date: Thu, 8 Jan 2026 21:34:26 +0000 Subject: [PATCH 4/8] [CI Pipeline] Released Snapshot version: 4.7.6-alpha-151-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 579d311..f4dc707 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.uid2 uid2-optout - 4.7.5 + 4.7.6-alpha-151-SNAPSHOT uid2-optout https://github.com/IABTechLab/uid2-optout From 5aac2e4d79e6edaa7a3d58652b0278eb049171e1 Mon Sep 17 00:00:00 2001 From: Caroline6312 Date: Thu, 8 Jan 2026 13:38:11 -0800 Subject: [PATCH 5/8] Just for test --- .trivyignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.trivyignore b/.trivyignore index ce9759b..5a14f02 100644 --- a/.trivyignore +++ b/.trivyignore @@ -12,4 +12,7 @@ CVE-2025-64720 exp:2026-06-05 CVE-2025-65018 exp:2026-06-05 # UID2-6385 -CVE-2025-66293 exp:2026-06-15 \ No newline at end of file +CVE-2025-66293 exp:2026-06-15 + +#for test +CVE-2025-68973 exp:2026-01-09 \ No newline at end of file From 1b1f05d2918efe7ca9eb9625c94ae8b4e09518fa Mon Sep 17 00:00:00 2001 From: Release Workflow Date: Thu, 8 Jan 2026 21:40:34 +0000 Subject: [PATCH 6/8] [CI Pipeline] Released Snapshot version: 4.7.7-alpha-152-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f4dc707..1ba0f67 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.uid2 uid2-optout - 4.7.6-alpha-151-SNAPSHOT + 4.7.7-alpha-152-SNAPSHOT uid2-optout https://github.com/IABTechLab/uid2-optout From 8ef7b656820e4bd9d4af01143f958dc10a11ebf1 Mon Sep 17 00:00:00 2001 From: Caroline6312 Date: Thu, 8 Jan 2026 14:53:22 -0800 Subject: [PATCH 7/8] Update --- .../java/com/uid2/optout/vertx/GenericFailureHandler.java | 3 +-- .../com/uid2/optout/vertx/GenericFailureHandlerTest.java | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/uid2/optout/vertx/GenericFailureHandler.java b/src/main/java/com/uid2/optout/vertx/GenericFailureHandler.java index 61cfe4c..19b5b09 100644 --- a/src/main/java/com/uid2/optout/vertx/GenericFailureHandler.java +++ b/src/main/java/com/uid2/optout/vertx/GenericFailureHandler.java @@ -43,8 +43,7 @@ public void handle(RoutingContext ctx) { errorMsg.equalsIgnoreCase( "Request method must be one of POST, PUT, PATCH or DELETE to decode a multipart request")) { if (!response.ended() && !response.closed()) { - response.setStatusCode(400) - .end("Bad Request: multipart content not allowed with this HTTP method"); + response.setStatusCode(400).end(errorMsg); } LOGGER.warn("URL: [{}] - Multipart method mismatch - Error:", url, t); return; diff --git a/src/test/java/com/uid2/optout/vertx/GenericFailureHandlerTest.java b/src/test/java/com/uid2/optout/vertx/GenericFailureHandlerTest.java index 4d4c469..9b16859 100644 --- a/src/test/java/com/uid2/optout/vertx/GenericFailureHandlerTest.java +++ b/src/test/java/com/uid2/optout/vertx/GenericFailureHandlerTest.java @@ -38,8 +38,8 @@ public void setup() { @Test public void testMultipartMethodMismatch_returns400() { - IllegalStateException exception = new IllegalStateException( - "Request method must be one of POST, PUT, PATCH or DELETE to decode a multipart request"); + String errorMsg = "Request method must be one of POST, PUT, PATCH or DELETE to decode a multipart request"; + IllegalStateException exception = new IllegalStateException(errorMsg); when(routingContext.statusCode()).thenReturn(-1); when(routingContext.failure()).thenReturn(exception); @@ -53,7 +53,7 @@ public void testMultipartMethodMismatch_returns400() { verify(response).end(bodyCaptor.capture()); assertEquals(400, statusCaptor.getValue()); - assertEquals("Bad Request: multipart content not allowed with this HTTP method", bodyCaptor.getValue()); + assertEquals(errorMsg, bodyCaptor.getValue()); } @Test From 1174679fef8bcac820442438b92388e4a6a2117d Mon Sep 17 00:00:00 2001 From: Release Workflow Date: Thu, 8 Jan 2026 22:55:28 +0000 Subject: [PATCH 8/8] [CI Pipeline] Released Snapshot version: 4.7.8-alpha-155-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 1ba0f67..5153cd7 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.uid2 uid2-optout - 4.7.7-alpha-152-SNAPSHOT + 4.7.8-alpha-155-SNAPSHOT uid2-optout https://github.com/IABTechLab/uid2-optout