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 96dd3cad7b655aed1c89b2428cf11648cd0bce9a
Author: opencode <[email protected]>
AuthorDate: Mon Oct 12 10:13:28 2026 +0200

    Correct the content type of WebDAV error reports
    
    The XML error reports (the multi-status response of a LOCK that
    conflicts with a locked child resource, and the two responses built by
    sendReport()) began with a prolog declaring the utf-8 encoding, yet
    they did not set a content type before using the response writer. The
    body was therefore written with the default body charset, ISO-8859-1,
    and the response carried no Content-Type header at all, leaving
    clients to guess how to decode a document whose own declaration
    contradicted the wire charset.
    
    These three paths now set the same "text/xml; charset=UTF-8" content
    type as the other WebDAV XML responses. The content type, which was
    previously repeated as a literal in the three XML response paths, is
    now shared through a constant. A regression test asserts the content
    type and the well-formedness of the three error reports.
---
 .../apache/catalina/servlets/WebdavServlet.java    |  15 ++-
 .../catalina/servlets/TestWebdavServlet.java       | 139 +++++++++++++++++++++
 2 files changed, 151 insertions(+), 3 deletions(-)

diff --git a/java/org/apache/catalina/servlets/WebdavServlet.java 
b/java/org/apache/catalina/servlets/WebdavServlet.java
index 3f104967e3..5eef729fa1 100644
--- a/java/org/apache/catalina/servlets/WebdavServlet.java
+++ b/java/org/apache/catalina/servlets/WebdavServlet.java
@@ -219,6 +219,12 @@ public class WebdavServlet extends DefaultServlet 
implements PeriodicEventListen
     protected static final String DEFAULT_NAMESPACE = "DAV:";
 
 
+    /**
+     * Content type used for the XML responses.
+     */
+    private static final String XML_CONTENT_TYPE = "text/xml; charset=UTF-8";
+
+
     /**
      * Pre generated raw XML for supported locks.
      */
@@ -973,7 +979,7 @@ public class WebdavServlet extends DefaultServlet 
implements PeriodicEventListen
 
         resp.setStatus(WebdavStatus.SC_MULTI_STATUS);
 
-        resp.setContentType("text/xml; charset=UTF-8");
+        resp.setContentType(XML_CONTENT_TYPE);
 
         // Create multistatus object
         XMLWriter generatedXML = new XMLWriter(resp.getWriter());
@@ -1204,7 +1210,7 @@ public class WebdavServlet extends DefaultServlet 
implements PeriodicEventListen
 
         resp.setStatus(WebdavStatus.SC_MULTI_STATUS);
 
-        resp.setContentType("text/xml; charset=UTF-8");
+        resp.setContentType(XML_CONTENT_TYPE);
 
         // Create multistatus object
         XMLWriter generatedXML = new XMLWriter(resp.getWriter());
@@ -1700,6 +1706,7 @@ public class WebdavServlet extends DefaultServlet 
implements PeriodicEventListen
 
                     generatedXML.writeElement("D", "multistatus", 
XMLWriter.CLOSING);
 
+                    resp.setContentType(XML_CONTENT_TYPE);
                     Writer writer = resp.getWriter();
                     writer.write(generatedXML.toString());
                     writer.close();
@@ -1822,7 +1829,7 @@ public class WebdavServlet extends DefaultServlet 
implements PeriodicEventListen
 
         generatedXML.writeElement("D", "prop", XMLWriter.CLOSING);
 
-        resp.setContentType("text/xml; charset=UTF-8");
+        resp.setContentType(XML_CONTENT_TYPE);
         Writer writer = resp.getWriter();
         writer.write(generatedXML.toString());
         writer.close();
@@ -2550,6 +2557,7 @@ public class WebdavServlet extends DefaultServlet 
implements PeriodicEventListen
 
         generatedXML.writeElement("D", "multistatus", XMLWriter.CLOSING);
 
+        resp.setContentType(XML_CONTENT_TYPE);
         Writer writer = resp.getWriter();
         writer.write(generatedXML.toString());
         writer.close();
@@ -2577,6 +2585,7 @@ public class WebdavServlet extends DefaultServlet 
implements PeriodicEventListen
 
         generatedXML.writeElement("D", "error", XMLWriter.CLOSING);
 
+        resp.setContentType(XML_CONTENT_TYPE);
         Writer writer = resp.getWriter();
         writer.write(generatedXML.toString());
         writer.close();
diff --git a/test/org/apache/catalina/servlets/TestWebdavServlet.java 
b/test/org/apache/catalina/servlets/TestWebdavServlet.java
index a888c20185..70530eb57c 100644
--- a/test/org/apache/catalina/servlets/TestWebdavServlet.java
+++ b/test/org/apache/catalina/servlets/TestWebdavServlet.java
@@ -1594,6 +1594,130 @@ public class TestWebdavServlet extends TomcatBaseTest {
     }
 
 
+    /*
+     * The XML error reports start with a declaration that states UTF-8, so the
+     * response needs to name a content type with a matching charset. Without a
+     * content type the response writer falls back to the default body charset,
+     * which is ISO-8859-1.
+     */
+    @Test
+    public void testErrorReportsUseTheDeclaredCharset() throws Exception {
+        Tomcat tomcat = getTomcatInstance();
+
+        // Create a temp webapp that can be safely written to
+        File tempWebapp = new File(getTemporaryDirectory(), 
"webdav-errorreport");
+        tempWebapp.deleteOnExit();
+        Assert.assertTrue(tempWebapp.mkdirs());
+        Context ctxt = tomcat.addContext("", tempWebapp.getAbsolutePath());
+        Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new 
WebdavServlet());
+        webdavServlet.addInitParameter("listings", "true");
+        webdavServlet.addInitParameter("secret", "foo");
+        webdavServlet.addInitParameter("readonly", "false");
+        ctxt.addServletMapping("/*", "webdav");
+        tomcat.start();
+
+        Client client = new Client();
+        client.setPort(getPort());
+
+        // Create a collection
+        // @formatter:off
+        client.setRequest(new String[] {
+                "MKCOL /myfolder HTTP/1.1" + CRLF +
+                    "Host: localhost:" + getPort() + CRLF +
+                    "Connection: Close" + CRLF +
+                    CRLF
+                });
+        // @formatter:on
+        client.connect();
+        client.processRequest(true);
+        Assert.assertEquals(HttpServletResponse.SC_CREATED, 
client.getStatusCode());
+
+        // Create a file in the collection
+        // @formatter:off
+        client.setRequest(new String[] {
+                "PUT /myfolder/file.txt HTTP/1.1" + CRLF +
+                    "Host: localhost:" + getPort() + CRLF +
+                    "Content-Length: " + CONTENT.length() + CRLF +
+                    "Connection: Close" + CRLF +
+                    CRLF +
+                    CONTENT
+                });
+        // @formatter:on
+        client.connect();
+        client.processRequest(true);
+        Assert.assertEquals(HttpServletResponse.SC_CREATED, 
client.getStatusCode());
+
+        // Lock the file
+        // @formatter:off
+        client.setRequest(new String[] {
+                "LOCK /myfolder/file.txt HTTP/1.1" + CRLF +
+                    "Host: localhost:" + getPort() + CRLF +
+                    "Content-Length: " + LOCK_BODY.length() + CRLF +
+                    "Connection: Close" + CRLF +
+                    CRLF +
+                    LOCK_BODY
+                });
+        // @formatter:on
+        client.connect();
+        client.processRequest(true);
+        Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
+
+        // Unlock the file with a token that locks nothing, which is reported
+        // with an error document
+        // @formatter:off
+        client.setRequest(new String[] {
+                "UNLOCK /myfolder/file.txt HTTP/1.1" + CRLF +
+                    "Host: localhost:" + getPort() + CRLF +
+                    "Lock-Token: <my:token>" + CRLF +
+                    "Connection: Close" + CRLF +
+                    CRLF
+                });
+        // @formatter:on
+        client.connect();
+        client.processRequest(true);
+        Assert.assertEquals(HttpServletResponse.SC_CONFLICT, 
client.getStatusCode());
+        
Assert.assertTrue(client.getResponseBody().contains("<D:lock-token-matches-request-uri>"));
+        assertXmlContentType(client);
+        validateXml(client.getResponseBody());
+
+        // Lock the collection, which conflicts with the lock of the child
+        // file and is reported with a multi-status document
+        // @formatter:off
+        client.setRequest(new String[] {
+                "LOCK /myfolder HTTP/1.1" + CRLF +
+                    "Host: localhost:" + getPort() + CRLF +
+                    "Content-Length: " + LOCK_BODY.length() + CRLF +
+                    "Connection: Close" + CRLF +
+                    CRLF +
+                    LOCK_BODY
+                });
+        // @formatter:on
+        client.connect();
+        client.processRequest(true);
+        Assert.assertEquals(WebdavStatus.SC_MULTI_STATUS, 
client.getStatusCode());
+        
Assert.assertTrue(client.getResponseBody().contains("<D:href>/myfolder/file.txt</D:href>"));
+        assertXmlContentType(client);
+        validateXml(client.getResponseBody());
+
+        // Delete the collection, which cannot delete the locked file and is
+        // reported with a multi-status document
+        // @formatter:off
+        client.setRequest(new String[] {
+                "DELETE /myfolder HTTP/1.1" + CRLF +
+                    "Host: localhost:" + getPort() + CRLF +
+                    "Connection: Close" + CRLF +
+                    CRLF
+                });
+        // @formatter:on
+        client.connect();
+        client.processRequest(true);
+        Assert.assertEquals(WebdavStatus.SC_MULTI_STATUS, 
client.getStatusCode());
+        
Assert.assertTrue(client.getResponseBody().contains("<D:href>/myfolder/file.txt</D:href>"));
+        assertXmlContentType(client);
+        validateXml(client.getResponseBody());
+    }
+
+
     private void doTestLimit(String method, String requestBody) throws 
Exception {
 
         Tomcat tomcat = getTomcatInstance();
@@ -1722,6 +1846,21 @@ public class TestWebdavServlet extends TomcatBaseTest {
         SAXParserFactory.newInstance().newSAXParser().getXMLReader().parse(new 
InputSource(new StringReader(xmlContent)));
     }
 
+
+    private static void assertXmlContentType(SimpleHttpClient client) {
+        for (String header : client.getResponseHeaders()) {
+            if (header.regionMatches(true, 0, "Content-Type: ", 0, 
"Content-Type: ".length())) {
+                String contentType = header.substring("Content-Type: 
".length()).trim();
+                int semicolon = contentType.indexOf(';');
+                Assert.assertTrue("The content type has no charset: " + 
contentType, semicolon >= 0);
+                Assert.assertEquals("text/xml", contentType.substring(0, 
semicolon).trim());
+                Assert.assertEquals("charset=UTF-8", 
contentType.substring(semicolon + 1).trim());
+                return;
+            }
+        }
+        Assert.fail("The response has no content type");
+    }
+
     private static final class Client extends SimpleHttpClient {
 
         @Override


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

Reply via email to