Skip to content

Conversation

@slachiewicz
Copy link
Member

  • Handle 409 Conflict status when parent collections don't exist
  • Handle redirects on MKCOL operations
  • Accept 200 OK status for already-existing collections
  • Fix tests to use WebDAV PROPFIND instead of filesystem checks

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the WebDavWagon implementation to comply with RFC 4918 WebDAV specifications, improving the robustness of collection creation operations and refactoring tests to use proper WebDAV PROPFIND requests instead of direct filesystem checks.

  • Enhances MKCOL operation to handle additional RFC 4918 status codes (200 OK, 409 Conflict)
  • Adds redirect handling for MKCOL requests
  • Introduces a new collectionExists method for WebDAV-based collection verification

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
WebDavWagon.java Added RFC 4918 compliance for status code handling in mkdirs(), implemented redirect handling in doMkCol(), and added collectionExists() method for WebDAV-based collection verification
WebDavWagonTest.java Refactored testMkdirs() and testMkdirsWithNoBasedir() to use WebDAV PROPFIND via collectionExists() instead of filesystem File.exists() checks

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +155 to +162
if (statusCode >= HttpStatus.SC_MULTIPLE_CHOICES && statusCode < HttpStatus.SC_BAD_REQUEST) {
org.apache.http.Header locationHeader = closeableHttpResponse.getFirstHeader("Location");
if (locationHeader != null) {
String redirectUrl = locationHeader.getValue();
// Recursive call to handle redirect - execute() will handle the redirect automatically
// but we need to return the final status
return doMkCol(redirectUrl);
}
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The redirect handling logic has a potential infinite recursion issue. If a server responds with a redirect that points back to the original URL or creates a redirect loop, this will cause a stack overflow. The recursive call should include protection against redirect loops, such as tracking visited URLs or limiting the maximum number of redirects.

Copilot uses AI. Check for mistakes.
// RFC 4918: 409 Conflict means intermediate collection is missing, continue traversing backwards
if (status == HttpStatus.SC_CONFLICT) {
continue;
}
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The backward traversal loop doesn't handle unexpected status codes properly. If doMkCol returns a status code that is not CREATED, OK, METHOD_NOT_ALLOWED, or CONFLICT (e.g., 403 Forbidden, 401 Unauthorized, 500 Internal Server Error), the loop will continue traversing backwards indefinitely until reaching the root. This could lead to incorrect behavior or silent failures. Consider throwing an exception for unexpected error status codes.

Suggested change
}
}
// Any other status is unexpected here: stop traversal and report an error
throw new IOException("Unexpected status code while creating collection: " + url
+ "; status code = " + status);

Copilot uses AI. Check for mistakes.
Comment on lines +198 to +216
DavPropertyNameSet nameSet = new DavPropertyNameSet();
nameSet.add(DavPropertyName.create(DavConstants.PROPERTY_RESOURCETYPE));

CloseableHttpResponse closeableHttpResponse = null;
HttpPropfind method = null;
try {
method = new HttpPropfind(url, nameSet, DavConstants.DEPTH_0);
closeableHttpResponse = execute(method);
int statusCode = closeableHttpResponse.getStatusLine().getStatusCode();
return statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_MULTI_STATUS;
} catch (HttpException e) {
throw new IOException(e.getMessage(), e);
} finally {
if (method != null) {
method.releaseConnection();
}
if (closeableHttpResponse != null) {
closeableHttpResponse.close();
}
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The collectionExists method doesn't validate whether the resource is actually a collection. It returns true for any resource (file or collection) that returns OK or MULTI_STATUS. According to RFC 4918, PROPFIND should verify that the resourcetype property contains a collection element. Consider parsing the response to check if the resource is specifically a collection, similar to the isDirectory method implementation.

Suggested change
DavPropertyNameSet nameSet = new DavPropertyNameSet();
nameSet.add(DavPropertyName.create(DavConstants.PROPERTY_RESOURCETYPE));
CloseableHttpResponse closeableHttpResponse = null;
HttpPropfind method = null;
try {
method = new HttpPropfind(url, nameSet, DavConstants.DEPTH_0);
closeableHttpResponse = execute(method);
int statusCode = closeableHttpResponse.getStatusLine().getStatusCode();
return statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_MULTI_STATUS;
} catch (HttpException e) {
throw new IOException(e.getMessage(), e);
} finally {
if (method != null) {
method.releaseConnection();
}
if (closeableHttpResponse != null) {
closeableHttpResponse.close();
}
try {
return isDirectory(url);
} catch (DavException e) {
throw new IOException(e.getMessage(), e);

Copilot uses AI. Check for mistakes.
Comment on lines +159 to +160
// Recursive call to handle redirect - execute() will handle the redirect automatically
// but we need to return the final status
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on line 159 is inconsistent with the actual implementation. The comment states "execute() will handle the redirect automatically" but then the code makes a recursive call to handle the redirect manually. Either the redirect is handled automatically by the HTTP client (in which case the manual handling code is unnecessary), or it needs to be handled manually (in which case the comment is misleading). Clarify the comment to accurately reflect the implementation.

Suggested change
// Recursive call to handle redirect - execute() will handle the redirect automatically
// but we need to return the final status
// Follow the redirect by recursively invoking doMkCol on the new URL
// and return the status code from the final resolved location

Copilot uses AI. Check for mistakes.
Comment on lines 140 to 168
String repositoryUrl = testRepository.getUrl();

// test leading /
assertFalse(new File(dir, "foo").exists());
String fooUrl = repositoryUrl + (repositoryUrl.endsWith("/") ? "" : "/") + "foo";
assertFalse("Collection should not exist before creation", wagon.collectionExists(fooUrl));
wagon.mkdirs("/foo");
assertTrue(new File(dir, "foo").exists());
assertTrue("Collection should exist after creation", wagon.collectionExists(fooUrl));

// test trailing /
assertFalse(new File(dir, "bar").exists());
String barUrl = repositoryUrl + (repositoryUrl.endsWith("/") ? "" : "/") + "bar";
assertFalse("Collection should not exist before creation", wagon.collectionExists(barUrl));
wagon.mkdirs("bar/");
assertTrue(new File(dir, "bar").exists());
assertTrue("Collection should exist after creation", wagon.collectionExists(barUrl));

// test when already exists
// test when already exists (should not fail)
wagon.mkdirs("bar");
assertTrue("Collection should still exist", wagon.collectionExists(barUrl));

// test several parts
assertFalse(new File(dir, "1/2/3/4").exists());
String deepUrl = repositoryUrl + (repositoryUrl.endsWith("/") ? "" : "/") + "1/2/3/4";
assertFalse("Deep collection should not exist before creation", wagon.collectionExists(deepUrl));
wagon.mkdirs("1/2/3/4");
assertTrue(new File(dir, "1/2/3/4").exists());
assertTrue("Deep collection should exist after creation", wagon.collectionExists(deepUrl));

// test additional part and trailing /
assertFalse(new File(dir, "1/2/3/4/5").exists());
String deeperUrl = repositoryUrl + (repositoryUrl.endsWith("/") ? "" : "/") + "1/2/3/4/5";
assertFalse("Deeper collection should not exist before creation", wagon.collectionExists(deeperUrl));
wagon.mkdirs("1/2/3/4/5/");
assertTrue(new File(dir, "1/2/3/4").exists());
assertTrue("Deeper collection should exist after creation", wagon.collectionExists(deeperUrl));
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The URL construction pattern is repetitive and error-prone. The same logic for appending paths to repositoryUrl is duplicated multiple times. Consider extracting this into a helper method to reduce duplication and improve maintainability.

Copilot uses AI. Check for mistakes.
- Handle 409 Conflict status when parent collections don't exist
- Handle redirects on MKCOL operations
- Accept 200 OK status for already-existing collections
- Fix tests to use WebDAV PROPFIND instead of filesystem checks
@slachiewicz slachiewicz force-pushed the vk/03fd-wagon-583-webdav branch from f8c7ea0 to 3f095c3 Compare January 1, 2026 20:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant