From 9e13a4ce3d4e5c4ddd07526b6473b1719b4455f7 Mon Sep 17 00:00:00 2001 From: gilesb Date: Mon, 12 Jan 2026 14:46:55 +0000 Subject: [PATCH] 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 --- artdag.py | 87 +++++++++++++++++++++++-------------------------------- 1 file changed, 36 insertions(+), 51 deletions(-) diff --git a/artdag.py b/artdag.py index 920a5f1..06aae09 100755 --- a/artdag.py +++ b/artdag.py @@ -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 ", 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