Files
StarPunk/docs/decisions/ADR-039-micropub-url-construction-fix.md
Phil Skentelbery 82bb1499d5 docs: Add v1.1.0 architecture and validation documentation
- 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>
2025-11-25 10:39:58 -07:00

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:

  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:

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

  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:

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

  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: Location header requirements
  • IndieAuth Spec: Client ID URL requirements
  • ADR-028: Micropub Implementation Strategy
  • docs/standards/versioning-strategy.md: Version increment guidelines