HeyPuter/puter · error · HttpError

dest_is_not_a_directory

dest_is_not_a_directory

Error message

Destination parent missing or not a directory

What it means

Thrown by the WebDAV COPY handler when the destination's parent directory either does not exist or is not a directory (destParent is null or destParent.isDir is false). RFC 4918 §9.8.5 mandates a 409 Conflict in this situation — copy cannot create intermediate directories.

Solutions

  1. MKCOL the full parent directory chain for the Destination before issuing COPY.
  2. Confirm with PROPFIND that the destination parent exists and is a collection (isDir true).
  3. Use a Destination whose parent is a known existing directory.
  4. If the parent name collides with a file, rename the conflicting file or choose a different parent.

Example fix

// before
COPY /src.txt HTTP/1.1
Destination: /new-dir/sub/file.txt

// after
MKCOL /new-dir HTTP/1.1
MKCOL /new-dir/sub HTTP/1.1
COPY /src.txt HTTP/1.1
Destination: /new-dir/sub/file.txt
Defensive patterns

Strategy: validation

Validate before calling

const parent = pathPosix.dirname(destPath);
const parentEntry = await webdavPropfind(parent).catch(() => null);
if (!parentEntry?.isCollection) {
  // MKCOL the chain first
  await mkcolRecursive(parent);
}

Try / catch

try {
  await webdavCopy(src, dest, headers);
} catch (e) {
  if (e.status === 409) { await mkcolRecursive(pathPosix.dirname(dest)); await webdavCopy(src, dest, headers); }
  else throw e;
}

Prevention

When it happens

Trigger: A COPY whose Destination path's parent has not been created yet (e.g. copying to /a/b/c.txt when /a/b does not exist), or whose parent resolves to a file rather than a directory.

Common situations: Copying into a freshly chosen folder that was never MKCOL'd; destination parent was deleted between PROPFIND and COPY; path component collided with an existing file name; client assumed recursive folder creation.

Related errors


AI-assisted analysis of HeyPuter/puter@908ec23eda (2026-08-12). Data as JSON: /api/errors/d8b5513e446f630e. Report an issue: GitHub.

Appendix: source

Thrown at src/backend/controllers/webdav/WebDAVController.ts:666

        const source = await this.stores.fsEntry.getEntryByPath(davPath);
        if (!source)
            throw new HttpError(404, 'Source not found', {
                legacyCode: 'not_found',
            });

        const overwrite = req.headers.overwrite !== 'F';
        const destExists = await this.stores.fsEntry.getEntryByPath(destPath);
        if (destExists && !overwrite)
            throw new HttpError(412, 'Destination exists and Overwrite=F', {
                legacyCode: 'conflict',
            });

        const destParent = await this.stores.fsEntry.getEntryByPath(
            pathPosix.dirname(destPath),
        );
        if (!destParent?.isDir)
            throw new HttpError(
                409,
                'Destination parent missing or not a directory',
                { legacyCode: 'dest_is_not_a_directory' },
            );

        const copy = await this.services.fs.copy(userId, {
            source,
            destinationParent: destParent,
            newName: pathPosix.basename(destPath),
            overwrite,
        });
        this.#emitGuiEvent('outer.gui.item.added', copy);
        res.status(destExists ? 204 : 201).end();
    }

    // -- MOVE --------------------------------------------------------

    async #move(

View on GitHub (pinned to 908ec23eda)