# 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/", 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/` to return 404 for nonexistent notes - [ ] Verify `GET /admin/edit/` still returns 404 (already correct) - [ ] Update `POST /admin/delete/` 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/", 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/", 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/", 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