Fix content negotiation - default to HTML, not JSON

All public endpoints (/assets/{name}, /activities/{id}, /objects/{hash})
now default to HTML for browsers. JSON/ActivityStreams is only returned
when explicitly requested via Accept header. Fixes browser link clicks
showing JSON instead of HTML pages.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gilesb
2026-01-09 11:59:37 +00:00
parent a0d2328b01
commit c53f67fb19

View File

@@ -1652,22 +1652,28 @@ async def get_asset_by_name_legacy(name: str):
@app.get("/assets/{name}") @app.get("/assets/{name}")
async def get_asset(name: str, request: Request): async def get_asset(name: str, request: Request):
"""Get asset by name. HTML for browsers, JSON for APIs.""" """Get asset by name. HTML for browsers (default), JSON only if explicitly requested."""
registry = await load_registry() registry = await load_registry()
# Check if JSON explicitly requested
accept = request.headers.get("accept", "")
wants_json = "application/json" in accept and "text/html" not in accept
if name not in registry.get("assets", {}): if name not in registry.get("assets", {}):
if wants_html(request): if wants_json:
content = f''' raise HTTPException(404, f"Asset not found: {name}")
<h2 class="text-xl font-semibold text-white mb-4">Asset Not Found</h2> content = f'''
<p class="text-gray-400">No asset named "{name}" exists.</p> <h2 class="text-xl font-semibold text-white mb-4">Asset Not Found</h2>
<p class="mt-4"><a href="/assets" class="text-blue-400 hover:text-blue-300">&larr; Back to Assets</a></p> <p class="text-gray-400">No asset named "{name}" exists.</p>
''' <p class="mt-4"><a href="/assets" class="text-blue-400 hover:text-blue-300">&larr; Back to Assets</a></p>
return HTMLResponse(base_html("Asset Not Found", content, get_user_from_cookie(request))) '''
raise HTTPException(404, f"Asset not found: {name}") return HTMLResponse(base_html("Asset Not Found", content, get_user_from_cookie(request)))
if wants_html(request): if wants_json:
return await ui_asset_detail(name, request) return registry["assets"][name]
return registry["assets"][name] # Default to HTML for browsers
return await ui_asset_detail(name, request)
@app.get("/assets/by-run-id/{run_id}") @app.get("/assets/by-run-id/{run_id}")
@@ -2372,26 +2378,30 @@ async def get_activities(request: Request, page: int = 1, limit: int = 20):
@app.get("/activities/{activity_index}") @app.get("/activities/{activity_index}")
async def get_activity_detail(activity_index: int, request: Request): async def get_activity_detail(activity_index: int, request: Request):
"""Get single activity. HTML for browsers, JSON for APIs.""" """Get single activity. HTML for browsers (default), JSON only if explicitly requested."""
activities = await load_activities() activities = await load_activities()
# Check if JSON explicitly requested
accept = request.headers.get("accept", "")
wants_json = ("application/json" in accept or "application/activity+json" in accept) and "text/html" not in accept
if activity_index < 0 or activity_index >= len(activities): if activity_index < 0 or activity_index >= len(activities):
if wants_html(request): if wants_json:
content = ''' raise HTTPException(404, "Activity not found")
<h2 class="text-xl font-semibold text-white mb-4">Activity Not Found</h2> content = '''
<p class="text-gray-400">This activity does not exist.</p> <h2 class="text-xl font-semibold text-white mb-4">Activity Not Found</h2>
<p class="mt-4"><a href="/activities" class="text-blue-400 hover:text-blue-300">&larr; Back to Activities</a></p> <p class="text-gray-400">This activity does not exist.</p>
''' <p class="mt-4"><a href="/activities" class="text-blue-400 hover:text-blue-300">&larr; Back to Activities</a></p>
return HTMLResponse(base_html("Activity Not Found", content, get_user_from_cookie(request))) '''
raise HTTPException(404, "Activity not found") return HTMLResponse(base_html("Activity Not Found", content, get_user_from_cookie(request)))
activity = activities[activity_index] activity = activities[activity_index]
if wants_html(request): if wants_json:
# Reuse the UI activity detail logic return activity
return await ui_activity_detail(activity_index, request)
return activity # Default to HTML for browsers
return await ui_activity_detail(activity_index, request)
@app.get("/activity/{activity_index}") @app.get("/activity/{activity_index}")
@@ -2408,12 +2418,12 @@ async def get_object(content_hash: str, request: Request):
# Find asset by hash # Find asset by hash
for name, asset in registry.get("assets", {}).items(): for name, asset in registry.get("assets", {}).items():
if asset.get("content_hash") == content_hash: if asset.get("content_hash") == content_hash:
# Check Accept header for content negotiation # Check Accept header - only return JSON if explicitly requested
accept = request.headers.get("accept", "") accept = request.headers.get("accept", "")
wants_html = "text/html" in accept and "application/json" not in accept and "application/activity+json" not in accept wants_json = ("application/json" in accept or "application/activity+json" in accept) and "text/html" not in accept
if wants_html: if not wants_json:
# Redirect to detail page for browsers # Default: redirect to detail page for browsers
return RedirectResponse(url=f"/assets/{name}", status_code=303) return RedirectResponse(url=f"/assets/{name}", status_code=303)
owner = asset.get("owner", "unknown") owner = asset.get("owner", "unknown")