# 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

```python
@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)`:

```python
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)

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

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

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

- Test failure: `test_update_nonexistent_note_404` in `tests/test_routes_admin.py:386`
- Architectural review: `/home/phil/Projects/starpunk/docs/reviews/error-handling-rest-vs-web-patterns.md`
- RFC 7231 Section 6.5.4 (404 Not Found): https://tools.ietf.org/html/rfc7231#section-6.5.4
- IndieWeb Micropub spec: https://micropub.spec.indieweb.org/
- Flask documentation on status codes: https://flask.palletsprojects.com/en/latest/quickstart/#about-responses

## 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:

```python
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:

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