# 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: 1. **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/` 2. **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 3. **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 ## 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: ```python permalink = f"{site_url}/notes/{note.slug}" ``` To: ```python permalink = f"{site_url}notes/{note.slug}" ``` This works because SITE_URL is guaranteed to have a trailing slash. ### Affected Code Locations 1. `starpunk/micropub.py` line 311 - Location header in `handle_create` 2. `starpunk/micropub.py` line 381 - URL in Microformats2 response in `handle_query` ## Rationale ### Why Not Strip the Trailing Slash? We could follow the RSS feed approach and strip the trailing slash: ```python 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 ```python 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 ```python 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 ```python 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 1. Verify Location header has single slash 2. Test with various SITE_URL configurations (with/without trailing slash) 3. Ensure RSS feed still works correctly 4. 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](https://www.w3.org/TR/micropub/): Location header requirements - [IndieAuth Spec](https://indieauth.spec.indieweb.org/): Client ID URL requirements - ADR-028: Micropub Implementation Strategy - docs/standards/versioning-strategy.md: Version increment guidelines