Fix JSON decode error by adding Accept header to all API requests

The CLI was getting empty responses from the server because it wasn't
sending Accept: application/json header. The server uses content
negotiation and returns HTML for browser requests.

Changes:
- Updated get_auth_header() to always include Accept: application/json
- Simplified all commands to use get_auth_header(require_token=True)
- Removed redundant token_data checks now handled by get_auth_header

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gilesb
2026-01-12 14:46:55 +00:00
parent 84acfc45cf
commit 9e13a4ce3d

View File

@@ -53,18 +53,22 @@ def clear_token():
TOKEN_FILE.unlink()
def get_auth_header() -> dict:
"""Get Authorization header if token exists."""
def get_auth_header(require_token: bool = False) -> dict:
"""Get headers for API requests. Always includes Accept: application/json."""
headers = {"Accept": "application/json"}
token_data = load_token()
token = token_data.get("access_token")
if token:
return {"Authorization": f"Bearer {token}"}
return {}
headers["Authorization"] = f"Bearer {token}"
elif require_token:
click.echo("Not logged in. Use 'artdag login' first.", err=True)
sys.exit(1)
return headers
def api_get(path: str, auth: bool = False):
"""GET request to server."""
headers = get_auth_header() if auth else {}
headers = get_auth_header(require_token=auth)
resp = requests.get(f"{get_server()}{path}", headers=headers)
resp.raise_for_status()
return resp.json()
@@ -72,7 +76,7 @@ def api_get(path: str, auth: bool = False):
def api_post(path: str, data: dict = None, params: dict = None, auth: bool = False):
"""POST request to server."""
headers = get_auth_header() if auth else {}
headers = get_auth_header(require_token=auth)
resp = requests.post(f"{get_server()}{path}", json=data, params=params, headers=headers)
resp.raise_for_status()
return resp.json()
@@ -263,7 +267,7 @@ def stats():
sys.exit(1)
try:
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
headers = get_auth_header(require_token=True)
resp = requests.get(f"{get_server()}/api/stats", headers=headers)
resp.raise_for_status()
stats = resp.json()
@@ -294,7 +298,7 @@ def clear_data(force):
# Show current stats first
try:
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
headers = get_auth_header(require_token=True)
resp = requests.get(f"{get_server()}/api/stats", headers=headers)
resp.raise_for_status()
stats = resp.json()
@@ -409,13 +413,9 @@ def run(recipe, input_hash, name, wait):
@click.option("--offset", "-o", default=0, help="Offset for pagination")
def list_runs(limit, offset):
"""List all runs with pagination."""
token_data = load_token()
if not token_data.get("access_token"):
click.echo("Not logged in. Use 'artdag login' first.", err=True)
sys.exit(1)
headers = get_auth_header(require_token=True)
try:
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
resp = requests.get(f"{get_server()}/runs?offset={offset}&limit={limit}", headers=headers)
resp.raise_for_status()
data = resp.json()
@@ -449,10 +449,7 @@ def list_runs(limit, offset):
@click.option("--analysis", is_flag=True, help="Show audio analysis data")
def status(run_id, plan, artifacts, analysis):
"""Get status of a run with optional detailed views."""
token_data = load_token()
headers = {}
if token_data.get("access_token"):
headers["Authorization"] = f"Bearer {token_data['access_token']}"
headers = get_auth_header() # Optional auth, always has Accept header
try:
resp = requests.get(f"{get_server()}/runs/{run_id}", headers=headers)
@@ -606,7 +603,7 @@ def delete_run(run_id, force):
return
try:
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
headers = get_auth_header(require_token=True)
resp = requests.delete(f"{get_server()}/runs/{run_id}", headers=headers)
if resp.status_code == 400:
click.echo(f"Cannot delete: {resp.json().get('detail', 'Unknown error')}", err=True)
@@ -645,7 +642,7 @@ def delete_cache(cid, force):
return
try:
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
headers = get_auth_header(require_token=True)
resp = requests.delete(f"{get_server()}/cache/{cid}", headers=headers)
if resp.status_code == 400:
click.echo(f"Cannot delete: {resp.json().get('detail', 'Unknown error')}", err=True)
@@ -703,16 +700,12 @@ def matches_media_type(item: dict, media_type: str) -> bool:
default="all", help="Filter by media type")
def cache(limit, offset, media_type):
"""List cached content with pagination and optional type filter."""
token_data = load_token()
if not token_data.get("access_token"):
click.echo("Not logged in. Use 'artdag login' first.", err=True)
sys.exit(1)
headers = get_auth_header(require_token=True)
# Fetch more items if filtering to ensure we get enough results
fetch_limit = limit * 3 if media_type != "all" else limit
try:
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
resp = requests.get(f"{get_server()}/cache?offset={offset}&limit={fetch_limit}", headers=headers)
resp.raise_for_status()
data = resp.json()
@@ -817,7 +810,7 @@ def upload(filepath):
try:
with open(filepath, "rb") as f:
files = {"file": (Path(filepath).name, f)}
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
headers = get_auth_header(require_token=True)
resp = requests.post(f"{get_server()}/cache/upload", files=files, headers=headers)
if resp.status_code == 401:
click.echo("Authentication failed. Please login again.", err=True)
@@ -915,7 +908,7 @@ def meta(cid, origin, origin_url, origin_note, description, tags, folder, add_co
click.echo("Not logged in. Please run: artdag login <username>", err=True)
sys.exit(1)
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
headers = get_auth_header(require_token=True)
# Handle publish action
if publish_name:
@@ -1269,7 +1262,7 @@ def storage_list():
sys.exit(1)
try:
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
headers = get_auth_header(require_token=True)
resp = requests.get(f"{get_server()}/storage", headers=headers)
resp.raise_for_status()
data = resp.json()
@@ -1322,7 +1315,7 @@ def storage_add(provider_type, name, capacity):
# Send to server
try:
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
headers = get_auth_header(require_token=True)
payload = {
"provider_type": provider_type,
"config": config,
@@ -1355,7 +1348,7 @@ def storage_test(storage_id):
sys.exit(1)
try:
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
headers = get_auth_header(require_token=True)
resp = requests.post(f"{get_server()}/storage/{storage_id}/test", headers=headers)
if resp.status_code == 404:
click.echo(f"Storage provider not found: {storage_id}", err=True)
@@ -1389,7 +1382,7 @@ def storage_delete(storage_id, force):
return
try:
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
headers = get_auth_header(require_token=True)
resp = requests.delete(f"{get_server()}/storage/{storage_id}", headers=headers)
if resp.status_code == 400:
click.echo(f"Error: {resp.json().get('detail', 'Bad request')}", err=True)
@@ -1458,7 +1451,7 @@ def upload_recipe(filepath):
try:
with open(filepath, "rb") as f:
files = {"file": (Path(filepath).name, f)}
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
headers = get_auth_header(require_token=True)
resp = requests.post(f"{get_server()}/recipes/upload", files=files, headers=headers)
if resp.status_code == 401:
click.echo("Authentication failed. Please login again.", err=True)
@@ -1499,7 +1492,7 @@ def upload_effect(filepath):
try:
with open(filepath, "rb") as f:
files = {"file": (Path(filepath).name, f)}
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
headers = get_auth_header(require_token=True)
resp = requests.post(f"{get_server()}/effects/upload", files=files, headers=headers)
if resp.status_code == 401:
click.echo("Authentication failed. Please login again.", err=True)
@@ -1530,13 +1523,9 @@ def upload_effect(filepath):
@click.option("--offset", "-o", default=0, help="Offset for pagination")
def list_effects(limit, offset):
"""List uploaded effects with pagination."""
token_data = load_token()
if not token_data.get("access_token"):
click.echo("Not logged in. Use 'artdag login' first.", err=True)
sys.exit(1)
headers = get_auth_header(require_token=True)
try:
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
resp = requests.get(f"{get_server()}/effects?offset={offset}&limit={limit}", headers=headers)
resp.raise_for_status()
result = resp.json()
@@ -1577,7 +1566,7 @@ def show_effect(cid, source):
sys.exit(1)
try:
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
headers = get_auth_header(require_token=True)
resp = requests.get(f"{get_server()}/effects/{cid}", headers=headers)
if resp.status_code == 404:
click.echo(f"Effect not found: {cid}", err=True)
@@ -1644,13 +1633,9 @@ def show_effect(cid, source):
@click.option("--offset", "-o", default=0, help="Offset for pagination")
def list_recipes(limit, offset):
"""List uploaded recipes for the current user with pagination."""
token_data = load_token()
if not token_data.get("access_token"):
click.echo("Not logged in. Use 'artdag login' first.", err=True)
sys.exit(1)
headers = get_auth_header(require_token=True)
try:
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
resp = requests.get(f"{get_server()}/recipes?offset={offset}&limit={limit}", headers=headers)
resp.raise_for_status()
data = resp.json()
@@ -1688,7 +1673,7 @@ def show_recipe(recipe_id):
sys.exit(1)
try:
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
headers = get_auth_header(require_token=True)
resp = requests.get(f"{get_server()}/recipes/{recipe_id}", headers=headers)
if resp.status_code == 404:
click.echo(f"Recipe not found: {recipe_id}", err=True)
@@ -1745,7 +1730,7 @@ def run_recipe(recipe_id, inputs, wait):
# Run
try:
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
headers = get_auth_header(require_token=True)
resp = requests.post(
f"{get_server()}/recipes/{recipe_id}/run",
json={"inputs": input_dict},
@@ -1808,7 +1793,7 @@ def delete_recipe(recipe_id, force):
return
try:
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
headers = get_auth_header(require_token=True)
resp = requests.delete(f"{get_server()}/recipes/{recipe_id}", headers=headers)
if resp.status_code == 401:
click.echo("Authentication failed. Please login again.", err=True)
@@ -1872,7 +1857,7 @@ def generate_plan(recipe_file, inputs, features, output):
# Submit to API
try:
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
headers = get_auth_header(require_token=True)
resp = requests.post(
f"{get_server()}/api/v2/plan",
json=request_data,
@@ -1933,7 +1918,7 @@ def execute_plan(plan_file, wait):
# Submit to API
try:
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
headers = get_auth_header(require_token=True)
resp = requests.post(
f"{get_server()}/api/v2/execute",
json={"plan_json": plan_json},
@@ -2011,7 +1996,7 @@ def run_recipe_v2(recipe_file, inputs, features, wait):
click.echo(f"Inputs: {len(input_hashes)}")
try:
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
headers = get_auth_header(require_token=True)
resp = requests.post(
f"{get_server()}/api/v2/run-recipe",
json=request_data,
@@ -2046,7 +2031,7 @@ def run_recipe_v2(recipe_file, inputs, features, wait):
def _wait_for_v2_run(token_data: dict, run_id: str):
"""Poll v2 run status until completion."""
click.echo("Waiting for completion...")
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
headers = get_auth_header(require_token=True)
while True:
time.sleep(2)
@@ -2094,7 +2079,7 @@ def run_status_v2(run_id):
sys.exit(1)
try:
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
headers = get_auth_header(require_token=True)
resp = requests.get(
f"{get_server()}/api/v2/run/{run_id}",
headers=headers