This is an automated email from the ASF dual-hosted git repository.

rmaucher pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 45d38f87f888d6d00cca77626e78daf8a199343f
Author: opencode <[email protected]>
AuthorDate: Fri Oct 16 23:47:17 2026 +0200

    Report primitive message decode failures as DecodeException
    
    A @OnMessage method with a primitive (or boxed primitive) text payload
    parameter coerces the message via Util.coerceToType(), which throws
    NumberFormatException for invalid numeric input and
    StringIndexOutOfBoundsException for an empty message targeted at char.
    
    These unchecked exceptions escaped PojoMessageHandlerWholeText.decode(),
    passed through PojoMessageHandlerWholeBase.onMessage() which only
    catches DecodeException, and were handled by WsFrameBase as an
    unexpected failure: the endpoint's onError received the raw runtime
    exception and the connection was closed abnormally (1006).
    
    Convert the failure to a jakarta.websocket.DecodeException at the call
    site, matching how PojoMethodMapping already wraps the same call for
    path parameters. The endpoint's onError now receives a DecodeException
    and the connection stays open, as for decoder based endpoints.
    
    Add end-to-end regression tests covering an empty message for a char
    parameter and non-numeric text for an int parameter.
---
 .../tomcat/websocket/pojo/LocalStrings.properties  |   1 +
 .../pojo/PojoMessageHandlerWholeText.java          |   8 +-
 .../pojo/TestPojoMessageHandlerWholeText.java      | 154 +++++++++++++++++++++
 3 files changed, 162 insertions(+), 1 deletion(-)

diff --git a/java/org/apache/tomcat/websocket/pojo/LocalStrings.properties 
b/java/org/apache/tomcat/websocket/pojo/LocalStrings.properties
index dcd08eb745..bca0d92d52 100644
--- a/java/org/apache/tomcat/websocket/pojo/LocalStrings.properties
+++ b/java/org/apache/tomcat/websocket/pojo/LocalStrings.properties
@@ -22,6 +22,7 @@ pojoEndpointBase.onOpenFail=Failed to call onOpen method of 
POJO end point for P
 pojoMessageHandlerBase.onMessageFail=Exception during onMessage call to POJO 
of type [{0}] in session [{1}]
 
 pojoMessageHandlerWhole.decodeIoFail=IO error while decoding message
+pojoMessageHandlerWhole.decodePrimitiveFail=Failed to decode message value 
[{0}] to expected type [{1}]
 pojoMessageHandlerWhole.maxBufferSize=The maximum supported message size for 
this implementation is Integer.MAX_VALUE
 
 pojoMessageHandlerWholeBase.decodeDestroyFailed=Failed to destroy the decoder 
of type [{0}]
diff --git 
a/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeText.java 
b/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeText.java
index b25a5b2e13..15acf5db4f 100644
--- a/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeText.java
+++ b/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeText.java
@@ -105,7 +105,13 @@ public class PojoMessageHandlerWholeText extends 
PojoMessageHandlerWholeBase<Str
     protected Object decode(String message) throws DecodeException {
         // Handle primitives
         if (primitiveType != null) {
-            return Util.coerceToType(primitiveType, message);
+            try {
+                return Util.coerceToType(primitiveType, message);
+            } catch (Exception e) {
+                throw new DecodeException(message,
+                        
sm.getString("pojoMessageHandlerWhole.decodePrimitiveFail", message,
+                                primitiveType.getName()), e);
+            }
         }
         // Handle full decoders
         for (Decoder decoder : decoders) {
diff --git 
a/test/org/apache/tomcat/websocket/pojo/TestPojoMessageHandlerWholeText.java 
b/test/org/apache/tomcat/websocket/pojo/TestPojoMessageHandlerWholeText.java
new file mode 100644
index 0000000000..1d4e7c23ae
--- /dev/null
+++ b/test/org/apache/tomcat/websocket/pojo/TestPojoMessageHandlerWholeText.java
@@ -0,0 +1,154 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.tomcat.websocket.pojo;
+
+import java.net.URI;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+import jakarta.websocket.ContainerProvider;
+import jakarta.websocket.DecodeException;
+import jakarta.websocket.OnError;
+import jakarta.websocket.OnMessage;
+import jakarta.websocket.Session;
+import jakarta.websocket.WebSocketContainer;
+import jakarta.websocket.server.ServerEndpoint;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.catalina.Context;
+import org.apache.catalina.servlets.DefaultServlet;
+import org.apache.catalina.startup.Tomcat;
+import org.apache.catalina.startup.TomcatBaseTest;
+import org.apache.tomcat.websocket.pojo.TesterUtil.ServerConfigListener;
+import org.apache.tomcat.websocket.pojo.TesterUtil.SingletonConfigurator;
+
+public class TestPojoMessageHandlerWholeText extends TomcatBaseTest {
+
+    private static final long TIMEOUT_SECONDS = 10;
+
+    /*
+     * A @OnMessage method with a char parameter cannot be satisfied by an 
empty text message. That must be reported to
+     * the endpoint as a DecodeException via onError and must not close the 
connection.
+     */
+    @Test
+    public void testEmptyTextForCharParameter() throws Exception {
+        CharServer server = new CharServer();
+
+        Session session = connect(server, CharServer.class);
+
+        session.getBasicRemote().sendText("");
+
+        Throwable error = server.errors.poll(TIMEOUT_SECONDS, 
TimeUnit.SECONDS);
+        Assert.assertNotNull("No error reported to the endpoint", error);
+        Assert.assertTrue("Expected a DecodeException, got: " + error, error 
instanceof DecodeException);
+
+        // The connection must still be usable after the decode error.
+        session.getBasicRemote().sendText("z");
+        Character received = server.messages.poll(TIMEOUT_SECONDS, 
TimeUnit.SECONDS);
+        Assert.assertEquals("Endpoint did not receive the message sent after 
the decode error", Character.valueOf('z'),
+                received);
+
+        session.close();
+    }
+
+
+    /*
+     * A @OnMessage method with an int parameter cannot be satisfied by a 
non-numeric text message. That must be
+     * reported to the endpoint as a DecodeException via onError and must not 
close the connection.
+     */
+    @Test
+    public void testNonNumericTextForIntParameter() throws Exception {
+        IntServer server = new IntServer();
+
+        Session session = connect(server, IntServer.class);
+
+        session.getBasicRemote().sendText("abc");
+
+        Throwable error = server.errors.poll(TIMEOUT_SECONDS, 
TimeUnit.SECONDS);
+        Assert.assertNotNull("No error reported to the endpoint", error);
+        Assert.assertTrue("Expected a DecodeException, got: " + error, error 
instanceof DecodeException);
+
+        // The connection must still be usable after the decode error.
+        session.getBasicRemote().sendText("123");
+        Integer received = server.messages.poll(TIMEOUT_SECONDS, 
TimeUnit.SECONDS);
+        Assert.assertEquals("Endpoint did not receive the message sent after 
the decode error",
+                Integer.valueOf(123), received);
+
+        session.close();
+    }
+
+
+    private Session connect(Object pojo, Class<?> pojoClass) throws Exception {
+        SingletonConfigurator.setInstance(pojo);
+        ServerConfigListener.setPojoClazz(pojoClass);
+
+        Tomcat tomcat = getTomcatInstance();
+        // No file system docBase required
+        Context ctx = getProgrammaticRootContext();
+        ctx.addApplicationListener(ServerConfigListener.class.getName());
+        Tomcat.addServlet(ctx, "default", new DefaultServlet());
+        ctx.addServletMapping("/", "default");
+
+        WebSocketContainer wsContainer = 
ContainerProvider.getWebSocketContainer();
+
+        tomcat.start();
+
+        return wsContainer.connectToServer(new TesterUtil.SimpleClient(),
+                new URI("ws://localhost:" + getPort() + "/" + 
pojoClass.getSimpleName().toLowerCase()));
+    }
+
+
+    @ServerEndpoint(value = "/charserver", configurator = 
SingletonConfigurator.class)
+    public static class CharServer {
+
+        final BlockingQueue<Throwable> errors = new LinkedBlockingQueue<>();
+        final BlockingQueue<Character> messages = new LinkedBlockingQueue<>();
+
+        @OnMessage
+        public void text(char c) {
+            messages.add(Character.valueOf(c));
+        }
+
+
+        @OnError
+        public void error(Throwable t) {
+            errors.add(t);
+        }
+    }
+
+
+    @ServerEndpoint(value = "/intserver", configurator = 
SingletonConfigurator.class)
+    public static class IntServer {
+
+        final BlockingQueue<Throwable> errors = new LinkedBlockingQueue<>();
+        final BlockingQueue<Integer> messages = new LinkedBlockingQueue<>();
+
+        @OnMessage
+        public void text(int i) {
+            messages.add(Integer.valueOf(i));
+        }
+
+
+        @OnError
+        public void error(Throwable t) {
+            errors.add(t);
+        }
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to