Skip to content

fix: handle ENOTDIR in FileDataAccessor.getStats to prevent 500 on duplicate slug POST - #2152

Merged
joachimvh merged 1 commit into
CommunitySolidServer:mainfrom
timgent:fix-save-file-twice-bug
Apr 21, 2026
Merged

fix: handle ENOTDIR in FileDataAccessor.getStats to prevent 500 on duplicate slug POST#2152
joachimvh merged 1 commit into
CommunitySolidServer:mainfrom
timgent:fix-save-file-twice-bug

Conversation

@timgent

@timgent timgent commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

📁 Related issues

#2021

✍️ Description

Patch fix (I don't have permissions to add the label)

Posting a file to a container twice with the same Slug header causes a 500 InternalServerError on file-system-backed deployments (e.g. solidcommunity.net). The second POST succeeds on in-memory servers but crashes on disk-based ones.

Root cause: DataAccessorBasedStore.createSafeUri checks whether a resource already exists at the slug path by calling hasResource with both test.json and its container equivalent test.json/. On Linux/macOS, stat('test.json/') on a regular file returns the POSIX error ENOTDIR instead of ENOENT. FileDataAccessor.getStats only caught ENOENT and converted it to NotFoundHttpError; ENOTDIR escaped as a raw OS error, which ParsingHttpHandler then wrapped into a 500.

Call trace (before fix):

  1. POST /container/ with Slug: test.json (file already exists on disk)
  2. DataAccessorBasedStore.addResourcecreateSafeUri
  3. createSafeUri builds candidate path test.json, then checks hasResource({ path: 'test.json/' }) first (the container-equivalent check)
  4. hasResource calls FileDataAccessor.getMetadata({ path: 'test.json/' })
  5. getMetadata maps the URL to a filesystem path …/test.json/ and calls getStats
  6. getStats calls stat('…/test.json/') — Linux sees the trailing slash, tries to treat test.json as a directory, and throws ENOTDIR
  7. getStats only handles ENOENT; ENOTDIR is re-thrown as a raw OS error
  8. hasResource only catches NotFoundHttpError; raw ENOTDIR propagates out — the || short-circuits and the second check (test.json without slash) is never reached
  9. ParsingHttpHandler.handleError sees a non-HttpError and returns a 500

Fix: Also catch ENOTDIR in getStats and convert it to NotFoundHttpError. ENOTDIR on a trailing-slash path means "no container exists here" — semantically identical to not-found. With the fix:

  • hasResource({ path: 'test.json/' }) catches NotFoundHttpError → returns false
  • false || continues to hasResource({ path: 'test.json' }) → file exists → returns true
  • Condition is met → UUID fallback name is generated → 201

Result after fix:

  • First POST Slug: test.json201 at .../test.json
  • Second POST same slug → 201 at .../cb0ccbd8-f25a-... (UUID fallback)

✅ PR check list

Before this pull request can be merged, a core maintainer will check whether

  • this PR is labeled with the correct semver label
    • semver.patch: Backwards compatible bug fixes.
    • semver.minor: Backwards compatible feature additions.
    • semver.major: Breaking changes. This includes changing interfaces or configuration behaviour.
  • the correct branch is targeted. Patch updates can target main, other changes should target the latest versions/* branch.
  • the RELEASE_NOTES.md document in case of relevant feature or config changes.
  • any relevant documentation was updated to reflect the changes in this PR.

@timgent
timgent marked this pull request as draft April 17, 2026 21:42
@timgent
timgent marked this pull request as ready for review April 18, 2026 17:23
@timgent
timgent force-pushed the fix-save-file-twice-bug branch from 5cd43f6 to 2dc6306 Compare April 18, 2026 17:33

@joachimvh joachimvh left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice find! Can you just remove the RELEASE_NOTES changes from the PR. That file is more for the drastic changes, minor bugfixes get added to the automatically generated changelog.

…plicate slug

When a file already exists at a given path, stat()-ing that path with a
trailing slash (e.g. test.json/) causes the OS to return ENOTDIR instead
of ENOENT. This error was not caught by getStats, causing it to propagate
as a raw OS error and result in a 500 InternalServerError on the second
POST with the same Slug header to a file-system-backed server.

Treating ENOTDIR the same as ENOENT (i.e. converting it to
NotFoundHttpError) means hasResource correctly returns false for the
container-style path check, allowing createSafeUri to fall back to a
UUID-based name for the duplicate upload.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@timgent
timgent force-pushed the fix-save-file-twice-bug branch from 2dc6306 to 56688a4 Compare April 20, 2026 17:32
@timgent

timgent commented Apr 20, 2026

Copy link
Copy Markdown
Contributor Author

@joachimvh I've removed the update to the release notes - sorry about that!

@joachimvh joachimvh left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good, thanks!

@joachimvh
joachimvh merged commit 31794f3 into CommunitySolidServer:main Apr 21, 2026
30 checks passed
@timgent
timgent deleted the fix-save-file-twice-bug branch April 21, 2026 09:19
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.

2 participants