- ADR-033: Database migration redesign - ADR-034: Full-text search with FTS5 - ADR-035: Custom slugs in Micropub - ADR-036: IndieAuth token verification method - ADR-039: Micropub URL construction fix - Implementation plan and decisions - Architecture specifications - Validation reports for implementation and search UI 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
5.3 KiB
ADR-039: Micropub URL Construction Fix
Status
Accepted
Context
After the v1.0.0 release, a bug was discovered in the Micropub implementation where the Location header returned after creating a post contains a double slash:
- Expected:
https://starpunk.thesatelliteoflove.com/notes/so-starpunk-v100-is-complete - Actual:
https://starpunk.thesatelliteoflove.com//notes/so-starpunk-v100-is-complete
Root Cause Analysis
The issue occurs due to a mismatch between how SITE_URL is stored and used:
-
Configuration Storage (
starpunk/config.py):- SITE_URL is normalized to always end with a trailing slash (lines 26, 92)
- This is required for IndieAuth/OAuth specs where root URLs must have trailing slashes
- Example:
https://starpunk.thesatelliteoflove.com/
-
URL Construction (
starpunk/micropub.py):- Constructs URLs using:
f"{site_url}/notes/{note.slug}"(lines 311, 381) - This adds a leading slash to the path segment
- Results in:
https://starpunk.thesatelliteoflove.com/+/notes/...= double slash
- Constructs URLs using:
-
Inconsistent Handling:
- RSS feed module (
starpunk/feed.py) correctly strips trailing slash before use (line 77) - Micropub module doesn't handle this, causing the bug
- RSS feed module (
Decision
Fix the URL construction in the Micropub module by removing the leading slash from the path segment. This maintains the trailing slash convention in SITE_URL while ensuring correct URL construction.
Implementation Approach
Change the URL construction pattern from:
permalink = f"{site_url}/notes/{note.slug}"
To:
permalink = f"{site_url}notes/{note.slug}"
This works because SITE_URL is guaranteed to have a trailing slash.
Affected Code Locations
starpunk/micropub.pyline 311 - Location header inhandle_createstarpunk/micropub.pyline 381 - URL in Microformats2 response inhandle_query
Rationale
Why Not Strip the Trailing Slash?
We could follow the RSS feed approach and strip the trailing slash:
site_url = site_url.rstrip("/")
permalink = f"{site_url}/notes/{note.slug}"
However, this approach has downsides:
- Adds unnecessary processing to every request
- Creates inconsistency with how SITE_URL is used elsewhere
- The trailing slash is intentionally added for IndieAuth compliance
Why This Solution?
- Minimal change: Only modifies the string literal, not the logic
- Consistent: SITE_URL remains normalized with trailing slash throughout
- Efficient: No runtime string manipulation needed
- Clear intent: The code explicitly shows we expect SITE_URL to end with
/
Consequences
Positive
- Fixes the immediate bug with minimal code changes
- No configuration changes required
- No database migrations needed
- Backward compatible - doesn't break existing data
- Fast to implement and test
Negative
- Developers must remember that SITE_URL has a trailing slash
- Could be confusing without documentation
- Potential for similar bugs if pattern isn't followed elsewhere
Mitigation
- Add a comment at each URL construction site explaining the trailing slash convention
- Consider adding a utility function in future versions for URL construction
- Document the SITE_URL trailing slash convention clearly
Alternatives Considered
1. Strip Trailing Slash at Usage Site
site_url = current_app.config.get("SITE_URL", "http://localhost:5000").rstrip("/")
permalink = f"{site_url}/notes/{note.slug}"
- Pros: More explicit, follows RSS feed pattern
- Cons: Extra processing, inconsistent with config intention
2. Remove Trailing Slash from Configuration
Modify config.py to not add trailing slashes to SITE_URL.
- Pros: Simpler URL construction
- Cons: Breaks IndieAuth spec compliance, requires migration for existing deployments
3. Create URL Builder Utility
def build_url(base, *segments):
"""Build URL from base and path segments"""
return "/".join([base.rstrip("/")] + list(segments))
- Pros: Centralized URL construction, prevents future bugs
- Cons: Over-engineering for a simple fix, adds unnecessary abstraction for v1.0.1
4. Use urllib.parse.urljoin
from urllib.parse import urljoin
permalink = urljoin(site_url, f"notes/{note.slug}")
- Pros: Standard library solution, handles edge cases
- Cons: Adds import, slightly less readable, overkill for this use case
Implementation Notes
Version Impact
- Current version: v1.0.0
- Fix version: v1.0.1 (PATCH increment - backward-compatible bug fix)
Testing Requirements
- Verify Location header has single slash
- Test with various SITE_URL configurations (with/without trailing slash)
- Ensure RSS feed still works correctly
- Check all other URL constructions in the codebase
Release Type
This qualifies as a hotfix because:
- It fixes a bug in production (v1.0.0)
- The fix is isolated and low-risk
- No new features or breaking changes
- Critical for proper Micropub client operation
References
- [Issue Report]: Malformed redirect URL in Micropub implementation
- W3C Micropub Spec: Location header requirements
- IndieAuth Spec: Client ID URL requirements
- ADR-028: Micropub Implementation Strategy
- docs/standards/versioning-strategy.md: Version increment guidelines