fix: Handle EntityCreationError as conflict by phernandez · Pull Request #541 · basicmachines-co/basic-memory · GitHub
Skip to content

fix: Handle EntityCreationError as conflict - #541

Merged
phernandez merged 3 commits into
mainfrom
codex/entity-creation-error-info
Feb 4, 2026
Merged

fix: Handle EntityCreationError as conflict#541
phernandez merged 3 commits into
mainfrom
codex/entity-creation-error-info

Conversation

@phernandez

Copy link
Copy Markdown
Member

Summary\n- log EntityCreationError at INFO and return 409 conflict\n- include friendly detail message for existing notes\n- include formatting fix in tests\n\n## Testing\n- not run (not requested)

Signed-off-by: phernandez <paul@basicmachines.co>
@github-actions

github-actions Bot commented Feb 4, 2026

Copy link
Copy Markdown
Contributor

@phernandez phernandez changed the title Handle EntityCreationError as conflict fix: Handle EntityCreationError as conflict Feb 4, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ba29d52036

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/api/app.py Outdated
Comment on lines +97 to +116
@app.exception_handler(EntityCreationError)
async def entity_creation_error_handler(request, exc): # pragma: no cover
"""Handle entity creation conflicts (e.g., file already exists).

This is expected behavior when users try to create notes that exist,
so log at INFO level instead of ERROR.
"""
logger.info(
"Entity already exists",
url=str(request.url),
method=request.method,
path=request.url.path,
error=str(exc),
)
return await http_exception_handler(
request,
HTTPException(
status_code=409,
detail="Note already exists. Use edit_note to modify it, or delete it first.",
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Return 409 only for actual conflicts

This handler treats every EntityCreationError as an “already exists” conflict, but EntityCreationError is also raised for non-conflict failures (e.g., create_entity_from_markdown wraps any upsert_entity exception into EntityCreationError). In those cases clients will now receive a 409 with a misleading “Note already exists” message instead of a 500, and logs will be INFO, which can mask real storage or database failures. Consider narrowing this to only the file-exists case (e.g., a specific exception subtype) or preserving the original error status for unexpected creation failures.

Useful? React with 👍 / 👎.

Signed-off-by: phernandez <paul@basicmachines.co>
@github-actions

github-actions Bot commented Feb 4, 2026

Copy link
Copy Markdown
Contributor

Claude finished @phernandez's task in 2m 12s —— View job


Reviewing PR #541: Handle EntityCreationError as conflict

Todo List

  • Read modified files and understand changes
  • Review changes against PR diff
  • Check against Basic Memory team checklist
  • Assess previous review feedback
  • Provide comprehensive feedback

Summary

This PR improves error handling for entity creation conflicts:

Commit 1 (ba29d52):

  • Added custom exception handler for EntityCreationError in app.py:98-117
  • Logs at INFO level (not ERROR) for conflicts
  • Returns 409 Conflict with user-friendly message
  • Added # pragma: no cover to the exception handler
  • Formatting improvements in test_search_service.py (lines 348-350, 369-371, 401-403)

Commit 2 (718ba6c):

  • Added type hints to exception handler (Request, EntityCreationError)
  • Added integration test test_create_entity_conflict_returns_409 in test_knowledge_router.py:185-210
  • Removed # pragma: no cover from exception handler

Review Against Team Checklist

✅ Code Quality & Standards

  • Follows Basic Memory's coding conventions in CLAUDE.md
  • Python 3.12+ type annotations - FIXED in commit 2 (added Request and EntityCreationError type hints)
  • FastAPI conventions followed
  • 100-character line length limit maintained
  • No commented-out code blocks

✅ Testing & Documentation

  • Integration test added for the new exception handler
  • 100% test coverage maintained - FIXED in commit 2 (pragma removed, test added)
  • Test coverage for edge cases
  • Documentation updated (docstring explains the handler's purpose)

✅ Basic Memory Architecture

  • Preserves local-first architecture principles
  • Proper error handling and logging at appropriate level
  • No impact to knowledge graph operations

✅ Security & Performance

  • Proper error handling
  • Appropriate logging (INFO for expected errors)
  • Performance considerations - minimal impact

✅ Compatibility

  • No file path comparisons in this change
  • No emojis or unicode in console/log output

Critical Issue: Codex Concern is Valid ⚠️

The Codex review raised an important concern about the exception handler treating all EntityCreationError exceptions as 409 Conflict responses. After reviewing the codebase, this concern is valid.

The Problem

EntityCreationError is raised in three different scenarios in entity_service.py:

  1. File already exists (lines 219, 359) - This is a conflict (409) ✅

    if await self.file_service.exists(file_path):
        raise EntityCreationError(f"file for entity... already exists: {file_path}")
  2. Database upsert failure (line 579) - This is NOT a conflict (should be 500) ❌

    try:
        return await self.repository.upsert_entity(model)
    except Exception as e:
        logger.error(f"Failed to upsert entity for {file_path}: {e}")
        raise EntityCreationError(f"Failed to create entity: {str(e)}") from e

The current handler treats all EntityCreationError as conflicts, including case #2 where it wraps any database exception. This means:

  • Database failures → 409 "Note already exists" (wrong!)
  • Permission errors → 409 "Note already exists" (wrong!)
  • Any upsert exception → 409 "Note already exists" (wrong!)

Why This Matters

  1. Misleading error messages: Users will see "Note already exists. Use edit_note..." when the real problem is a database failure
  2. Wrong status code: 409 indicates a client error (duplicate resource), but database failures are server errors (500)
  3. Logging at wrong level: INFO level for critical database failures masks real problems
  4. Harder debugging: Real issues will be buried in INFO logs instead of ERROR logs

The Solution

You have a few options:

Option 1: Separate exception types (recommended)

# In exceptions.py
class EntityAlreadyExistsError(EntityCreationError):
    """Raised when an entity file already exists"""
    pass

# In entity_service.py:219, 359
if await self.file_service.exists(file_path):
    raise EntityAlreadyExistsError(f"file for entity... already exists: {file_path}")

# In app.py - only handle the specific exception
@app.exception_handler(EntityAlreadyExistsError)
async def entity_already_exists_handler(request: Request, exc: EntityAlreadyExistsError):
    # ... 409 logic

Option 2: Add a flag to EntityCreationError

class EntityCreationError(Exception):
    def __init__(self, message: str, is_conflict: bool = False):
        super().__init__(message)
        self.is_conflict = is_conflict

# In app.py handler
if not exc.is_conflict:
    # Re-raise or return 500
    return await http_exception_handler(request, HTTPException(status_code=500, detail=str(exc)))

Option 3: Check the error message (not recommended - brittle)

# In app.py handler
if "already exists" not in str(exc).lower():
    # Return 500 for non-conflict errors

Recommendation

I recommend Option 1 because it:

  • Follows the Liskov Substitution Principle
  • Makes the code's intent explicit
  • Allows different handling for different error types
  • Doesn't break existing code (EntityAlreadyExistsError is-a EntityCreationError)

Fix this →


Other Notes

Positive Aspects ✅

  • User-friendly error message guides users to edit_note
  • Appropriate HTTP status code (409) for actual conflicts
  • Clean separation of concerns in exception handling
  • Good test coverage with integration test
  • Fixed type hints and test coverage issues from previous review

Test Improvements (optional)

The integration test is good but could be enhanced:

# Additional test case to verify logging level
@pytest.mark.asyncio
async def test_create_entity_conflict_logs_at_info_level(client, v2_project_url, caplog):
    """Verify that conflicts are logged at INFO, not ERROR."""
    # Create entity twice and check logs contain INFO level message

Final Verdict

🚫 DO NOT MERGE until the Codex concern is addressed.

The current implementation will treat all entity creation failures as conflicts, including critical database failures. This will:

  1. Return misleading 409 responses for 500-level errors
  2. Log critical failures at INFO level instead of ERROR
  3. Make debugging production issues much harder

Please implement one of the solutions above (preferably Option 1) to distinguish between actual conflicts (file exists) and other creation failures (database errors, permissions, etc.).


Branch Information

Signed-off-by: phernandez <paul@basicmachines.co>
@github-actions

github-actions Bot commented Feb 4, 2026

Copy link
Copy Markdown
Contributor

@phernandez
phernandez merged commit 343a6e1 into main Feb 4, 2026
28 checks passed
@phernandez
phernandez deleted the codex/entity-creation-error-info branch February 4, 2026 04:29
@phernandez phernandez added this to the v0.19.0 milestone Feb 26, 2026
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