Files
StarPunk/docs/decisions/ADR-012-http-error-handling-policy.md
Phil Skentelbery 0cca8169ce feat: Implement Phase 4 Web Interface with bugfixes (v0.5.2)
## Phase 4: Web Interface Implementation

Implemented complete web interface with public and admin routes,
templates, CSS, and development authentication.

### Core Features

**Public Routes**:
- Homepage with recent published notes
- Note permalinks with microformats2
- Server-side rendering (Jinja2)

**Admin Routes**:
- Login via IndieLogin
- Dashboard with note management
- Create, edit, delete notes
- Protected with @require_auth decorator

**Development Authentication**:
- Dev login bypass for local testing (DEV_MODE only)
- Security safeguards per ADR-011
- Returns 404 when disabled

**Templates & Frontend**:
- Base layouts (public + admin)
- 8 HTML templates with microformats2
- Custom responsive CSS (114 lines)
- Error pages (404, 500)

### Bugfixes (v0.5.1 → v0.5.2)

1. **Cookie collision fix (v0.5.1)**:
   - Renamed auth cookie from "session" to "starpunk_session"
   - Fixed redirect loop between dev login and admin dashboard
   - Flask's session cookie no longer conflicts with auth

2. **HTTP 404 error handling (v0.5.1)**:
   - Update route now returns 404 for nonexistent notes
   - Delete route now returns 404 for nonexistent notes
   - Follows ADR-012 HTTP Error Handling Policy
   - Pattern consistency across all admin routes

3. **Note model enhancement (v0.5.2)**:
   - Exposed deleted_at field from database schema
   - Enables soft deletion verification in tests
   - Follows ADR-013 transparency principle

### Architecture

**New ADRs**:
- ADR-011: Development Authentication Mechanism
- ADR-012: HTTP Error Handling Policy
- ADR-013: Expose deleted_at Field in Note Model

**Standards Compliance**:
- Uses uv for Python environment
- Black formatted, Flake8 clean
- Follows git branching strategy
- Version incremented per versioning strategy

### Test Results

- 405/406 tests passing (99.75%)
- 87% code coverage
- All security tests passing
- Manual testing confirmed working

### Documentation

- Complete implementation reports in docs/reports/
- Architecture reviews in docs/reviews/
- Design documents in docs/design/
- CHANGELOG updated for v0.5.2

### Files Changed

**New Modules**:
- starpunk/dev_auth.py
- starpunk/routes/ (public, admin, auth, dev_auth)

**Templates**: 10 files (base, pages, admin, errors)
**Static**: CSS and optional JavaScript
**Tests**: 4 test files for routes and templates
**Docs**: 20+ architectural and implementation documents

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 23:01:53 -07:00

9.8 KiB

ADR-012: HTTP Error Handling Policy

Status

Accepted

Context

During Phase 4 (Web Interface) implementation, a test failure revealed inconsistent error handling between GET and POST routes when accessing nonexistent resources:

  • GET /admin/edit/99999 returns HTTP 404 (correct)
  • POST /admin/edit/99999 returns HTTP 302 redirect (incorrect)

This inconsistency creates several problems:

  1. Semantic confusion: HTTP 404 means "Not Found", but we were redirecting instead
  2. API incompatibility: Future Micropub API implementation requires proper HTTP status codes
  3. Debugging difficulty: Hard to distinguish between "note doesn't exist" and "operation failed"
  4. Test suite inconsistency: Tests expect 404, implementation returns 302

Traditional Web App Pattern

Many traditional web applications use:

  • 404 for GET: Can't render a form for nonexistent resource
  • 302 redirect for POST: Show user-friendly error message via flash

This provides good UX but sacrifices HTTP semantic correctness.

REST/API Pattern

REST APIs consistently use:

  • 404 for all operations on nonexistent resources
  • Applies to GET, POST, PUT, DELETE, etc.

This provides semantic correctness and API compatibility.

StarPunk's Requirements

  1. Human-facing web interface (Phase 4)
  2. Future Micropub API endpoint (Phase 5)
  3. Single-user system (simpler error handling needs)
  4. Standards compliance (IndieWeb specs require proper HTTP)

Decision

StarPunk will use REST-style error handling for all routes, returning HTTP 404 for any operation on a nonexistent resource, regardless of HTTP method.

Specific Rules

  1. All routes MUST return 404 when the target resource does not exist
  2. All routes SHOULD check resource existence before processing the request
  3. 404 responses MAY include user-friendly flash messages for web routes
  4. 404 responses MAY redirect to a safe location (e.g., dashboard) while still returning 404 status

Implementation Pattern

@bp.route("/operation/<int:resource_id>", methods=["GET", "POST"])
@require_auth
def operation(resource_id: int):
    # 1. CHECK EXISTENCE FIRST
    resource = get_resource(id=resource_id)
    if not resource:
        flash("Resource not found", "error")
        return redirect(url_for("admin.dashboard")), 404  # ← MUST return 404

    # 2. VALIDATE INPUT (for POST/PUT)
    # ...

    # 3. PERFORM OPERATION
    # ...

    # 4. RETURN SUCCESS
    # ...

Status Code Policy

Scenario Status Code Response Type Flash Message
Resource not found 404 Redirect to dashboard "Resource not found"
Validation failed 302 Redirect to form "Invalid data: {details}"
Operation succeeded 302 Redirect to dashboard "Success: {details}"
System error 500 Error page "System error occurred"
Unauthorized 302 Redirect to login "Authentication required"

Flask Pattern for 404 with Redirect

Flask allows returning a tuple (response, status_code):

return redirect(url_for("admin.dashboard")), 404

This sends:

  • HTTP 404 status code
  • Location header pointing to dashboard
  • Flash message in session

The client receives 404 but can follow the redirect to see the error message.

Rationale

Why REST-Style Over Web-Form-Style?

  1. Future API Compatibility: Micropub (Phase 5) requires proper HTTP semantics
  2. Standards Compliance: IndieWeb specs expect REST-like behavior
  3. Semantic Correctness: 404 means "not found" - this is universally understood
  4. Consistency: Simpler mental model - all operations follow same rules
  5. Debugging: Clear distinction between error types
  6. Test Intent: Test suite already expects this behavior

UX Considerations

Concern: Won't users see ugly error pages?

Mitigation:

  1. Flash messages provide context ("Note not found")
  2. 404 response includes redirect to dashboard
  3. Can implement custom 404 error handler with navigation
  4. Single-user system = developer is the user (understands HTTP)

Comparison to Delete Operation

The delete_note() function is idempotent - it succeeds even if the note doesn't exist. This is correct for delete operations (common REST pattern). However, the route should still check existence and return 404 for consistency:

  • Idempotent implementation: Good (delete succeeds either way)
  • Explicit existence check in route: Better (clear 404 for user)

Consequences

Positive

  1. Consistent behavior across all routes (GET, POST, DELETE)
  2. API-ready: Micropub implementation will work correctly
  3. Standards compliance: Meets IndieWeb/REST expectations
  4. Better testing: Status codes clearly indicate error types
  5. Clearer debugging: Know immediately if resource doesn't exist
  6. Simpler code: One pattern to follow everywhere

Negative

  1. Requires existence checks: Every route must check before operating
  2. Slight performance cost: Extra database query per request (minimal for SQLite)
  3. Different from some web apps: Traditional web apps often use redirects for all POST errors

Neutral

  1. Custom 404 handler needed: For good UX (but we'd want this anyway)
  2. Test suite updates: Some tests may need adjustment (but most already expect 404)
  3. Documentation: Need to document this pattern (but good practice anyway)

Implementation Checklist

Immediate (Phase 4 Fix)

  • Fix POST /admin/edit/<id> to return 404 for nonexistent notes
  • Verify GET /admin/edit/<id> still returns 404 (already correct)
  • Update POST /admin/delete/<id> to return 404 (optional, but recommended)
  • Update test test_delete_nonexistent_note_shows_error if delete route changed

Future (Phase 4 Completion)

  • Create custom 404 error handler with navigation
  • Document pattern in /home/phil/Projects/starpunk/docs/standards/http-error-handling.md
  • Review all routes for consistency
  • Add error handling section to coding standards

Phase 5 (Micropub API)

  • Verify Micropub routes follow this pattern
  • Return JSON error responses for API routes
  • Maintain 404 status codes for missing resources

Examples

Good Example: Edit Note Form (GET)

@bp.route("/edit/<int:note_id>", methods=["GET"])
@require_auth
def edit_note_form(note_id: int):
    note = get_note(id=note_id)

    if not note:
        flash("Note not found", "error")
        return redirect(url_for("admin.dashboard")), 404  # ✓ CORRECT

    return render_template("admin/edit.html", note=note)

Status: Currently implemented correctly

Bad Example: Update Note (POST) - Before Fix

@bp.route("/edit/<int:note_id>", methods=["POST"])
@require_auth
def update_note_submit(note_id: int):
    # ✗ NO EXISTENCE CHECK

    try:
        note = update_note(id=note_id, content=content, published=published)
        # ...
    except Exception as e:
        flash(f"Error: {e}", "error")
        return redirect(url_for("admin.edit_note_form", note_id=note_id))  # ✗ Returns 302

Problem: Returns 302 redirect, not 404

Good Example: Update Note (POST) - After Fix

@bp.route("/edit/<int:note_id>", methods=["POST"])
@require_auth
def update_note_submit(note_id: int):
    # ✓ CHECK EXISTENCE FIRST
    existing_note = get_note(id=note_id, load_content=False)
    if not existing_note:
        flash("Note not found", "error")
        return redirect(url_for("admin.dashboard")), 404  # ✓ CORRECT

    # Process the update
    # ...

Status: Needs implementation

References

Alternatives Considered

Alternative 1: Web-Form Pattern (Redirect All POST Errors)

Rejected because:

  • Breaks semantic HTTP (404 means "not found")
  • Incompatible with future Micropub API
  • Makes debugging harder (can't distinguish error types by status code)
  • Test suite already expects 404

Alternative 2: Hybrid Approach (404 for API, 302 for Web)

Rejected because:

  • Adds complexity (need to detect context)
  • Inconsistent behavior confuses developers
  • Same route may serve both web and API clients
  • Flask blueprint structure makes this awkward

Alternative 3: Exception-Based (Let Exceptions Propagate to Error Handler)

Rejected because:

  • Less explicit (harder to understand flow)
  • Error handlers are global (less flexibility per route)
  • Flash messages harder to customize per route
  • Lose ability to redirect to different locations per route

Notes

Performance Consideration

The existence check adds one database query per request:

existing_note = get_note(id=note_id, load_content=False)  # SELECT query

With load_content=False, this is just a metadata query (no file I/O):

  • SQLite query: ~0.1ms for indexed lookup
  • Negligible overhead for single-user system
  • Could be optimized later if needed (caching, etc.)

Future Enhancement: Error Handler

Custom 404 error handler can improve UX:

@app.errorhandler(404)
def not_found(error):
    """Custom 404 page with navigation"""
    # Check if there's a flash message (from routes)
    # Render custom template with link to dashboard
    # Or redirect to dashboard for admin routes
    return render_template('errors/404.html'), 404

This is optional but recommended for Phase 4 completion.

Revision History

  • 2025-11-18: Initial decision (v0.4.0 development)
  • Status: Accepted
  • Supersedes: None
  • Related: ADR-003 (Frontend Technology), Phase 4 Design