Skip to content

fix: Handle EntityCreationError as conflict#541

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

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

Conversation

@phernandez
Copy link
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
Copy link
Contributor

github-actions bot commented Feb 4, 2026

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


PR Review Complete

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
  • Provide comprehensive feedback

Summary

This PR improves error handling for entity creation conflicts by:

  1. Adding a custom exception handler for EntityCreationError that returns 409 Conflict with a user-friendly message
  2. Logging at INFO level instead of ERROR (since conflicts are expected user behavior)
  3. Including minor formatting fixes in test file

Review Against Team Checklist

✅ Code Quality & Standards

  • Follows Basic Memory's coding conventions in CLAUDE.md
  • Python 3.12+ type annotations - but see note below about missing type hints
  • FastAPI conventions followed
  • 100-character line length limit maintained
  • No commented-out code blocks

⚠️ Testing & Documentation

  • CRITICAL: Missing test coverage for the new exception handler (see details below)
  • Test coverage for edge cases (formatting fixes are covered)
  • 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

Issues Found

1. CRITICAL: Missing Test Coverage

The new entity_creation_error_handler is marked with # pragma: no cover, but according to CLAUDE.md:

Coverage must stay at 100%: Write tests for new code. Only use # pragma: no cover when tests would require excessive mocking (e.g., TYPE_CHECKING blocks, error handlers that need failure injection, runtime-mode-dependent code paths)

The issue: This exception handler is testable and should have a test. It's not overly difficult to test - you just need an integration test that calls the API endpoint with a duplicate entity.

Where to add test:

  • Location: test-int/mcp/test_write_note_integration.py (line reference)
  • Test case: Create an entity, then try to create it again and verify:
    • Status code is 409
    • Response detail matches "Note already exists. Use edit_note to modify it, or delete it first."
    • Logs show INFO level (not ERROR)

Example test structure:

@pytest.mark.asyncio
async def test_write_note_duplicate_returns_409_conflict(mcp_test_context):
    """Test that creating a duplicate note returns 409 Conflict."""
    # Create initial note
    result1 = await write_note(...)
    
    # Try to create same note again
    with pytest.raises(ToolError) as exc_info:
        result2 = await write_note(...)
    
    # Verify error message and status
    assert "Note already exists" in str(exc_info.value)
    # Could also check logs for INFO level

Fix this →

2. Minor: Missing Type Hints ⚠️

In app.py:98, the exception handler parameters are missing type hints:

async def entity_creation_error_handler(request, exc):  # pragma: no cover

Should be:

async def entity_creation_error_handler(request: Request, exc: EntityCreationError):  # pragma: no cover

This would require adding from fastapi import Request to imports.

Fix this →

3. Note: Test Formatting Changes ℹ️

The test formatting changes in tests/services/test_search_service.py (lines 348-350, 369-371, 401-403) are fine - they improve readability by moving the error message to the same line as the assertion. These are covered by existing tests.


Recommendation

Do not merge until:

  1. ✅ Add integration test for the exception handler (remove # pragma: no cover after adding test)
  2. ⚠️ (Optional but recommended) Add type hints to the handler

Positive aspects:

  • ✅ Good separation of concerns - conflict errors logged at INFO instead of ERROR
  • ✅ User-friendly error message guides users to the correct action
  • ✅ Follows FastAPI exception handler patterns
  • ✅ Appropriate HTTP status code (409 Conflict)

The core logic is sound, but the missing test coverage is a blocker per the team's 100% coverage requirement.


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

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

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 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.",
),

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
Copy link
Contributor

github-actions bot commented Feb 4, 2026

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
Copy link
Contributor

github-actions bot commented Feb 4, 2026

Claude finished @phernandez's task in 1m 50s —— View job


Reviewing PR #541: Handle EntityCreationError as conflict

Todo List

  • Read modified files and understand changes
  • Check latest commit addressing Codex concern
  • Review changes against PR diff
  • Check against Basic Memory team checklist
  • Provide comprehensive feedback

Summary

This PR successfully addresses entity creation conflicts with a three-commit evolution:

Commit 1 (ba29d52): Initial implementation

  • Added exception handler for EntityCreationError returning 409
  • Logged at INFO level instead of ERROR
  • Had # pragma: no cover and missing type hints
  • Issue: Treated ALL EntityCreationError as conflicts (including DB failures)

Commit 2 (718ba6c): Addressed initial review feedback

  • Added type hints (Request, EntityCreationError)
  • Added integration test test_create_entity_conflict_returns_409
  • Removed # pragma: no cover
  • Issue: Still treated all EntityCreationError as conflicts

Commit 3 (8aecae3): Addressed Codex critical concern ✅

  • Created EntityAlreadyExistsError subclass of EntityCreationError (exceptions.py:19-22)
  • Updated exception handler to catch only EntityAlreadyExistsError (app.py:97-119)
  • Updated both raise sites in entity_service.py to use EntityAlreadyExistsError (lines 223, 363)
  • Database failures now correctly raise EntityCreationError and return 500 (line 583)

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, EntityAlreadyExistsError types)
  • FastAPI conventions followed (proper exception handler pattern)
  • 100-character line length limit maintained
  • No commented-out code blocks

✅ Testing & Documentation

  • Integration test added for entity conflict scenario (test_knowledge_router.py:185-210)
  • 100% test coverage maintained - FIXED in commit 2 (removed # pragma: no cover, added test)
  • Test coverage for edge cases (formatting fixes are covered by existing tests)
  • Documentation updated (docstring explains handler's purpose and INFO logging rationale)

✅ Basic Memory Architecture

  • Preserves local-first architecture principles
  • Proper error handling with appropriate status codes:
    • 409 for conflicts (file already exists)
    • 500 for server errors (database failures)
  • Appropriate logging levels:
    • INFO for expected user behavior (conflicts)
    • ERROR for unexpected failures (database errors)
  • No impact to knowledge graph operations

✅ Security & Performance

  • Proper error handling with specific exception types
  • Appropriate logging (INFO for conflicts, ERROR for DB failures)
  • User-friendly error messages guide users to correct action
  • Performance considerations - minimal impact

✅ Compatibility

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

Critical Issue Resolution ✅

The Codex concern about treating all EntityCreationError as conflicts has been fully resolved in commit 8aecae3.

What Was Fixed

Before (commits 1-2):

@app.exception_handler(EntityCreationError)  # ❌ Too broad
async def entity_creation_error_handler(request: Request, exc: EntityCreationError):
    # All EntityCreationError → 409 (wrong for DB failures!)

After (commit 3):

@app.exception_handler(EntityAlreadyExistsError)  # ✅ Specific
async def entity_already_exists_error_handler(request: Request, exc: EntityAlreadyExistsError):
    # Only file-exists conflicts → 409

Implementation Details

  1. New exception subclass (exceptions.py:19-22):

    class EntityAlreadyExistsError(EntityCreationError):
        """Raised when an entity file already exists"""
  2. Both conflict locations updated (entity_service.py:223, 363):

    if await self.file_service.exists(file_path):
        raise EntityAlreadyExistsError(...)  # ✅ Specific exception
  3. Database failures still use base exception (entity_service.py:583):

    except Exception as e:
        logger.error(f"Failed to upsert entity for {file_path}: {e}")
        raise EntityCreationError(...)  # ✅ Falls through to 500 handler
  4. Test coverage validates the fix (test_knowledge_router.py:185-210):

    • Creates duplicate entity
    • Verifies 409 status code
    • Validates friendly error message

Code Quality Notes

Excellent Patterns ✅

  1. Proper exception hierarchy: EntityAlreadyExistsError extends EntityCreationError, following Liskov Substitution Principle
  2. Clear docstring: Explains WHY INFO logging is appropriate (expected user behavior)
  3. Structured logging: Uses keyword arguments for better log analysis
  4. User-friendly messaging: Guides users to the correct action ("Use edit_note...")
  5. Test formatting improvements: Lines 348-350, 369-371, 401-403 in test_search_service.py improve readability

Minor Observations

  1. Exception handler line 98-100: Type hints are properly added ✅
  2. No pragma needed: Exception handler is now tested ✅
  3. HTTP status codes: Correctly differentiated:
    • 409 Conflict for file-exists (client can fix by deleting/editing)
    • 500 Internal Server Error for DB failures (server issue)

Test Coverage Analysis

Integration Test (test_knowledge_router.py:185-210)

async def test_create_entity_conflict_returns_409(client: AsyncClient, v2_project_url):
    """Test creating a duplicate entity returns 409 Conflict."""

What it tests:

  • ✅ Creates entity successfully first time (200)
  • ✅ Second creation returns 409
  • ✅ Error message matches expected friendly text
  • ✅ Exercises the new exception handler code path

Coverage: This test covers the entity_already_exists_error_handler (app.py:97-119), which is why the # pragma: no cover was removed.

Test Formatting Changes (test_search_service.py)

Lines 348-350, 369-371, 401-403: Moved assertion messages to same line for better readability. These changes are covered by existing boolean search tests.


Final Verdict

READY TO MERGE

All concerns from previous reviews have been addressed:

  1. Type hints added (commit 2)
  2. Test coverage added (commit 2)
  3. Pragma removed (commit 2)
  4. Codex concern resolved (commit 3) - Separate exception types prevent misclassifying DB failures as conflicts

Positive Highlights

  • Thoughtful evolution: PR addressed feedback incrementally and correctly
  • Proper exception design: EntityAlreadyExistsError subclass is the right pattern
  • Clear separation of concerns: Conflicts vs failures are now distinct at the type level
  • User experience: Friendly error messages guide users to correct action
  • Maintainability: Future developers can easily add more specific EntityCreationError subtypes
  • Observability: INFO vs ERROR logging helps distinguish expected vs unexpected failures

No Blockers Remaining

All checklist items pass. The implementation follows Basic Memory's conventions, maintains 100% test coverage, and properly handles both conflict and failure scenarios with appropriate status codes and logging levels.


View PR on GitHub

@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
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