You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by re...@apache.org on 2026/10/12 13:58:32 UTC
(tomcat) 01/22: Correct the content type of WebDAV error reports
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 <re...@apache.org>
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: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org