This is an automated email from the ASF dual-hosted git repository.
ffang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cxf.git
The following commit(s) were added to refs/heads/main by this push:
new 47dd3136f8a CXF-8542: Add a regression test for MTOM attachment stream
failures (#3593)
47dd3136f8a is described below
commit 47dd3136f8a9afd4fdda5fd2ecf717caa5b497d0
Author: Freeman(Yue) Fang <[email protected]>
AuthorDate: Fri Oct 16 11:05:36 2026 -0400
CXF-8542: Add a regression test for MTOM attachment stream failures (#3593)
If the InputStream of an MTOM attachment's DataHandler throws an
IOException while the server is writing the response, the client must
not receive the truncated data as if it were complete. This has been
fixed by CXF-9057, which aborts the response in that case.
The new test fails the attachment stream after 1 KB (response not yet
committed) and after 1 MB (response committed), with both the default
HttpClient conduit and the URLConnection conduit, and checks that the
client either gets an exception from the invocation or an IOException
while reading the attachment. Unlike the CXF-9057 tests, it uses an
IOException (as in the report) at a deterministic position. It fails
without the CXF-9057 changes.
---
.../jaxws/MtomAttachmentStreamFailureTest.java | 228 +++++++++++++++++++++
1 file changed, 228 insertions(+)
diff --git
a/systests/jaxws/src/test/java/org/apache/cxf/systest/jaxws/MtomAttachmentStreamFailureTest.java
b/systests/jaxws/src/test/java/org/apache/cxf/systest/jaxws/MtomAttachmentStreamFailureTest.java
new file mode 100644
index 00000000000..58c0bc4afa7
--- /dev/null
+++
b/systests/jaxws/src/test/java/org/apache/cxf/systest/jaxws/MtomAttachmentStreamFailureTest.java
@@ -0,0 +1,228 @@
+/**
+ * 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.cxf.systest.jaxws;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.logging.Logger;
+
+import jakarta.activation.DataHandler;
+import jakarta.activation.DataSource;
+import jakarta.xml.ws.BindingProvider;
+import jakarta.xml.ws.Endpoint;
+import jakarta.xml.ws.WebServiceException;
+import jakarta.xml.ws.soap.SOAPBinding;
+import org.apache.cxf.Download;
+import org.apache.cxf.DownloadNextResponseType;
+import org.apache.cxf.common.logging.LogUtils;
+import org.apache.cxf.jaxws.JaxWsProxyFactoryBean;
+import org.apache.cxf.testutil.common.AbstractBusClientServerTestBase;
+import org.apache.cxf.testutil.common.AbstractBusTestServerBase;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * CXF-8542: if the InputStream of an MTOM attachment's DataHandler throws an
IOException while
+ * the server is writing the response, the client must not receive the
truncated data as if it
+ * were complete.
+ */
+public class MtomAttachmentStreamFailureTest extends
AbstractBusClientServerTestBase {
+ static final String PORT =
allocatePort(MtomAttachmentStreamFailureTest.class);
+ private static final Logger LOG =
LogUtils.getLogger(MtomAttachmentStreamFailureTest.class);
+
+ /** Size of the attachment when no failure is simulated */
+ private static final int FULL_SIZE = 4 * 1024 * 1024;
+
+ public static class Server extends AbstractBusTestServerBase {
+ protected void run() {
+ String address = "http://localhost:" + PORT +
"/SoapContext/SoapPort";
+ Endpoint endpoint = Endpoint.publish(address, new
FailingDownloadImpl());
+ ((SOAPBinding)endpoint.getBinding()).setMTOMEnabled(true);
+ }
+
+ public static void main(String[] args) {
+ try {
+ Server s = new Server();
+ s.start();
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ System.exit(-1);
+ } finally {
+ LOG.info("done!");
+ }
+ }
+ }
+
+ /**
+ * Returns an attachment of FULL_SIZE bytes, or, if simulate is true, an
attachment whose
+ * InputStream throws an IOException after minNumberOfChunks KB have been
read.
+ */
+ static final class FailingDownloadImpl implements Download {
+ @Override
+ public DownloadNextResponseType downloadNext(Integer
minNumberOfChunks, Boolean simulate) {
+ final long size = Boolean.TRUE.equals(simulate) ?
minNumberOfChunks * 1024L : FULL_SIZE;
+ final boolean fail = Boolean.TRUE.equals(simulate);
+ DownloadNextResponseType response = new DownloadNextResponseType();
+ response.setDataContent(new DataHandler(new DataSource() {
+ @Override
+ public InputStream getInputStream() {
+ return new GeneratingInputStream(size, fail);
+ }
+
+ @Override
+ public OutputStream getOutputStream() {
+ return null;
+ }
+
+ @Override
+ public String getContentType() {
+ return "application/octet-stream";
+ }
+
+ @Override
+ public String getName() {
+ return "data";
+ }
+ }));
+ return response;
+ }
+ }
+
+ /**
+ * Generates size bytes, then either reports the end of the stream or
throws an IOException.
+ */
+ static final class GeneratingInputStream extends InputStream {
+ private final long size;
+ private final boolean fail;
+ private long count;
+
+ GeneratingInputStream(long size, boolean fail) {
+ this.size = size;
+ this.fail = fail;
+ }
+
+ @Override
+ public int read() throws IOException {
+ byte[] b = new byte[1];
+ return read(b, 0, 1) == -1 ? -1 : b[0];
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ if (count >= size) {
+ if (fail) {
+ throw new IOException("simulated I/O error after " + count
+ " bytes");
+ }
+ return -1;
+ }
+ int n = (int)Math.min(len, size - count);
+ for (int i = off; i < off + n; i++) {
+ b[i] = (byte)'x';
+ }
+ count += n;
+ return n;
+ }
+ }
+
+ @BeforeClass
+ public static void startServers() throws Exception {
+ assertTrue("server did not launch correctly",
launchServer(Server.class, true));
+ }
+
+ @Test
+ public void testFullDownload() throws Exception {
+ Download client = createClient(false);
+ DownloadNextResponseType response = client.downloadNext(0, false);
+ assertEquals(FULL_SIZE, readAll(response.getDataContent()));
+ }
+
+ @Test
+ public void testFailureAfterResponseCommitted() throws Exception {
+ // 1 MB is well beyond the servlet response buffer, so the response is
committed
+ assertFailureSignaled(createClient(false), 1024);
+ }
+
+ @Test
+ public void testFailureBeforeResponseCommitted() throws Exception {
+ // 1 KB still fits in the servlet response buffer
+ assertFailureSignaled(createClient(false), 1);
+ }
+
+ @Test
+ public void testFailureAfterResponseCommittedURLConnection() throws
Exception {
+ assertFailureSignaled(createClient(true), 1024);
+ }
+
+ @Test
+ public void testFailureBeforeResponseCommittedURLConnection() throws
Exception {
+ assertFailureSignaled(createClient(true), 1);
+ }
+
+ private static void assertFailureSignaled(Download client, int
failAfterKb) throws Exception {
+ final DownloadNextResponseType response;
+ try {
+ response = client.downloadNext(failAfterKb, true);
+ } catch (WebServiceException ex) {
+ // the response was aborted before the attachment could be read
+ LOG.fine("Failure signaled by the invocation: " + ex);
+ return;
+ }
+ final long read;
+ try {
+ read = readAll(response.getDataContent());
+ } catch (IOException ex) {
+ // the response was aborted while the attachment was being read
+ LOG.fine("Failure signaled while reading the attachment: " + ex);
+ return;
+ }
+ fail("The server failed after " + failAfterKb * 1024L + " bytes, but
the client read "
+ + read + " bytes without any error");
+ }
+
+ private Download createClient(boolean urlConnection) {
+ JaxWsProxyFactoryBean factory = new JaxWsProxyFactoryBean();
+ factory.setServiceClass(Download.class);
+ factory.setAddress("http://localhost:" + PORT +
"/SoapContext/SoapPort/DownloadPort");
+ Download client = (Download)factory.create();
+ BindingProvider bp = (BindingProvider)client;
+ ((SOAPBinding)bp.getBinding()).setMTOMEnabled(true);
+ if (urlConnection) {
+ bp.getRequestContext().put("force.urlconnection.http.conduit",
Boolean.TRUE);
+ }
+ return client;
+ }
+
+ private static long readAll(DataHandler dh) throws IOException {
+ long total = 0;
+ byte[] buf = new byte[8192];
+ try (InputStream in = dh.getInputStream()) {
+ for (int n = in.read(buf); n != -1; n = in.read(buf)) {
+ total += n;
+ }
+ }
+ return total;
+ }
+}