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 afd485ef2662bc8c42426004b2e5c8d52e086077 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 cc7d031e0d..d98ffd59e5 100644 --- a/java/org/apache/catalina/servlets/WebdavServlet.java +++ b/java/org/apache/catalina/servlets/WebdavServlet.java @@ -222,6 +222,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. */ @@ -968,7 +974,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()); @@ -1199,7 +1205,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()); @@ -1695,6 +1701,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(); @@ -1817,7 +1824,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(); @@ -2545,6 +2552,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(); @@ -2572,6 +2580,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 832722eb97..0f5500af2c 100644 --- a/test/org/apache/catalina/servlets/TestWebdavServlet.java +++ b/test/org/apache/catalina/servlets/TestWebdavServlet.java @@ -1595,6 +1595,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(); @@ -1723,6 +1847,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]
