This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 9.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 9abbc1b1db18898cc2e9d2dd4d19ef6b5cd7b7ed 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 52567beb7b..9945db9ca7 100644 --- a/java/org/apache/tomcat/websocket/pojo/LocalStrings.properties +++ b/java/org/apache/tomcat/websocket/pojo/LocalStrings.properties @@ -26,6 +26,7 @@ pojoMessageHandlerBase.encodeFail=Encoding failed for POJO of tyoe [{0}] in sess 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 a920d35081..4ea939a2ec 100644 --- a/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeText.java +++ b/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeText.java @@ -104,7 +104,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]
