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}")
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()
# 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 wants_html(request):
content = f'''
<h2 class="text-xl font-semibold text-white mb-4">Asset Not Found</h2>
<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}")
if wants_json:
raise HTTPException(404, f"Asset not found: {name}")
content = f'''
<h2 class="text-xl font-semibold text-white mb-4">Asset Not Found</h2>
<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)))
if wants_html(request):
return await ui_asset_detail(name, request)
if wants_json:
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}")
@@ -2372,26 +2378,30 @@ async def get_activities(request: Request, page: int = 1, limit: int = 20):
@app.get("/activities/{activity_index}")
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()
# 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 wants_html(request):
content = '''
<h2 class="text-xl font-semibold text-white mb-4">Activity Not Found</h2>
<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")
if wants_json:
raise HTTPException(404, "Activity not found")
content = '''
<h2 class="text-xl font-semibold text-white mb-4">Activity Not Found</h2>
<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)))
activity = activities[activity_index]
if wants_html(request):
# Reuse the UI activity detail logic
return await ui_activity_detail(activity_index, request)
if wants_json:
return activity
return activity
# Default to HTML for browsers
return await ui_activity_detail(activity_index, request)
@app.get("/activity/{activity_index}")
@@ -2408,12 +2418,12 @@ async def get_object(content_hash: str, request: Request):
# Find asset by hash
for name, asset in registry.get("assets", {}).items():
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", "")
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:
# Redirect to detail page for browsers
if not wants_json:
# Default: redirect to detail page for browsers
return RedirectResponse(url=f"/assets/{name}", status_code=303)
owner = asset.get("owner", "unknown")