## 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>
300 lines
9.8 KiB
Markdown
300 lines
9.8 KiB
Markdown
# 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
|