{"record":{"id":"c40e8fa6a2c6af70","repo":"harry0703/MoneyPrinterTurbo","slug":"sonilo-returned-an-invalid-service-response","errorCode":null,"errorMessage":"Sonilo returned an invalid service response","messagePattern":"Sonilo returned an invalid service response","errorType":"exception","errorClass":"SoniloError","httpStatus":null,"severity":"error","filePath":"app/services/sonilo.py","lineNumber":107,"sourceCode":"    if not api_key:\n        raise SoniloError(\"Sonilo API key is required\")\n    try:\n        response = requests.get(\n            f\"{_base_url()}{SERVICES_PATH}\",\n            headers={\"Authorization\": f\"Bearer {api_key}\"},\n            timeout=(15, 30),\n        )\n    except requests.RequestException as exc:\n        raise SoniloError(f\"failed to connect to Sonilo: {exc}\") from exc\n    if not response.ok:\n        raise SoniloError(\n            f\"Sonilo connection failed ({response.status_code}): \"\n            f\"{_safe_response_error(response)}\"\n        )\n    try:\n        payload = response.json()\n    except ValueError as exc:\n        raise SoniloError(\"Sonilo returned an invalid service response\") from exc\n    if not isinstance(payload, dict):\n        raise SoniloError(\"Sonilo returned an unexpected service response\")\n    available_services = payload.get(\"available_services\")\n    if not isinstance(available_services, list) or not all(\n        isinstance(service_id, str) for service_id in available_services\n    ):\n        raise SoniloError(\"Sonilo returned an invalid service list\")\n    normalized_services = {\n        _normalize_service_id(service_id) for service_id in available_services\n    }\n    if VIDEO_TO_MUSIC_SERVICE_ID not in normalized_services:\n        raise SoniloError(\"Sonilo video-to-music service is not available for this key\")\n    logger.info(\"Sonilo connection test succeeded\")\n    return payload\n\n\ndef _remove_file(file_path: str) -> None:\n    \"\"\"尽力清理 Sonilo 中间文件，不覆盖调用方正在处理的原始异常。\"\"\"","sourceCodeStart":89,"sourceCodeEnd":125,"githubUrl":"https://github.com/harry0703/MoneyPrinterTurbo/blob/1f9f19c2021a68d04df228f33e9099a0c947f6f8/app/services/sonilo.py#L89-L125","documentation":"Raised by sonilo.test_connection() when response.json() throws ValueError on the 2xx services response — the body is not valid JSON (HTML error page, empty body, gzip/proxy corruption). The original parse error is chained with `from exc`.","triggerScenarios":"requests.get to /v1/account/services returns 200 but body is HTML (captive portal, CDN error page), truncated (proxy buffering issue), or BOM-prefixed/plain text; response.json() then raises requests.exceptions.JSONDecodeError (a ValueError subclass).","commonSituations":"Captive portals / hotel WiFi injecting HTML on 200; transparent proxies mangling gzip encoding; Sonilo CDN edge returning an HTML error with status 200; sonilo_base_url pointing at a web UI host rather than the API host.","solutions":["Check sonilo_base_url — pointing at a web page host instead of the API host is the most common cause","Inspect the raw body: log response.headers.get('content-type') and response.text[:200] in a debug run to see HTML vs JSON","Bypass the captive/transparent proxy or fix proxy SSL/encoding handling","If content-type is consistently non-JSON from the API itself, the endpoint/path is wrong — use the default base_url"],"exampleFix":"# debug snippet to identify the offending body\nresp = requests.get(f\"{_base_url()}{SERVICES_PATH}\", headers=headers, timeout=(15, 30))\nprint(resp.status_code, resp.headers.get(\"content-type\"), resp.text[:200])","handlingStrategy":"validation","validationCode":"# pre-check that the endpoint returns JSON before parsing\nresp = requests.get(url, headers=headers, timeout=(15, 30))\nif 'application/json' not in (resp.headers.get('content-type') or ''):\n    raise SoniloError(f\"non-JSON content-type: {resp.headers.get('content-type')}\")","typeGuard":null,"tryCatchPattern":"except SoniloError as e: if 'invalid service response' in str(e): non-retryable in place — inspect content-type/body; if a proxy caused it, fix the proxy then retry","preventionTips":["Always check response content-type before .json() at third-party boundaries","Avoid custom base_url values pointing at non-API hosts","Log the first 200 bytes of unexpected bodies for triage"],"tags":["sonilo","json","response-parsing","proxy"],"backgroundTag":null,"analyzedSha":"1f9f19c2021a68d04df228f33e9099a0c947f6f8","analyzedAt":"2026-08-14T19:41:05.568Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}