apache/hadoop · error · IOException

Rename to subdir is forbidden

Error message

Rename to subdir is forbidden

What it means

For directory renames, getDstUri computes src.relativize(dst); if the result does not equal dst, then src is a parent (ancestor) directory of dst, meaning the rename would move a directory into its own subtree. That is logically impossible (would require moving objects under themselves) and is explicitly forbidden.

Source

Thrown at hadoop-cloud-storage-project/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/GoogleCloudStorageFileSystem.java:599

      // The first case (dst is an existing file) is already checked earlier.
      // If the destination path looks like a file, make it look like a
      // directory path. This is because users often type 'mv foo bar'
      // rather than 'mv foo bar/'.
      if (!dstInfo.isDirectory()) {
        dst = UriPaths.toDirectory(dst);
      }

      // Throw if renaming directory to self - this is forbidden
      if (src.equals(dst)) {
        throw new IOException("Rename dir to self is forbidden");
      }

      URI dstRelativeToSrc = src.relativize(dst);
      // Throw if dst URI relative to src is not equal to dst,
      // because this means that src is a parent directory of dst
      // and src cannot be "renamed" to its subdirectory
      if (!dstRelativeToSrc.equals(dst)) {
        throw new IOException("Rename to subdir is forbidden");
      }

      if (dstInfo.exists()) {
        dst =
            dst.equals(GCSROOT)
                ? UriPaths.fromStringPathComponents(
                srcItemName, /* objectName= */ null, /* allowEmptyObjectName= */ true)
                : UriPaths.toDirectory(dst.resolve(srcItemName));
      }
    } else {
      // -- src is a file
      //    -- dst is a file => rename the file.
      //    -- dst is a directory => similar to the previous case after
      //                             appending src file-name to dst

      if (dstInfo.isDirectory()) {
        if (!dstInfo.exists()) {
          throw new IOException("Cannot rename because path does not exist: " + dstInfo.getPath());

View on GitHub (pinned to 2add963021)

Solutions

  1. Choose a destination outside the source subtree, e.g. gs://b/renamed-foo instead of gs://b/foo/bar.
  2. To move data 'into itself' semantics, copy then delete: FileUtil.copy(fs, src, fs, dst, true, conf) followed by fs.delete(src, true).
  3. Add a guard rejecting dst that starts with src + '/' before calling rename.

Example fix

// before
fs.rename(new Path("gs://bucket/foo"), new Path("gs://bucket/foo/bar"));
// -> IOException: Rename to subdir is forbidden

// after: destination outside the source subtree
fs.rename(new Path("gs://bucket/foo"), new Path("gs://bucket/renamed-foo"));
Defensive patterns

Strategy: validation

Validate before calling

URI srcDir = UriPaths.toDirectory(src.toUri());
URI dstDir = UriPaths.toDirectory(dst.toUri());
boolean dstInsideSrc = dstDir.toString().startsWith(srcDir.toString() + "/") || dstDir.equals(srcDir);
if (!dstInsideSrc) {
  fs.rename(src, dst);
} else {
  // pick a destination outside the subtree, or copy + delete
}

Try / catch

catch IOException with message equals("Rename to subdir is forbidden") - restructure the move: rename to a sibling path, or FileUtil.copy then delete(src, true).

Prevention

When it happens

Trigger: rename(gs://b/foo, gs://b/foo/bar); rename(gs://b/foo, gs://b/foo/bar/baz); any destination whose path starts with the source directory path plus '/'.

Common situations: Data reorganization code that builds the destination as sourcePath + subpath by mistake; 'move into backup subfolder' logic like rename(dir, dir/backup); relative-path arithmetic bugs when composing URIs.

Understand the failure class

Related errors


AI-assisted analysis of apache/hadoop@2add963021 (2026-08-22). Data as JSON: /api/errors/ebaf3f8be78abd57. Report an issue: GitHub.