{"record":{"id":"6d831261e350114d","repo":"harry0703/MoneyPrinterTurbo","slug":"sonilo-returned-an-invalid-service-list","errorCode":null,"errorMessage":"Sonilo returned an invalid service list","messagePattern":"Sonilo returned an invalid service list","errorType":"exception","errorClass":"SoniloError","httpStatus":null,"severity":"error","filePath":"app/services/sonilo.py","lineNumber":114,"sourceCode":"        )\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 中间文件，不覆盖调用方正在处理的原始异常。\"\"\"\n    if not file_path or not os.path.exists(file_path):\n        return\n    try:\n        os.remove(file_path)\n    except OSError as exc:\n        logger.warning(\n            f\"failed to remove Sonilo temporary file: path={file_path}, error={exc}\"","sourceCodeStart":96,"sourceCodeEnd":132,"githubUrl":"https://github.com/harry0703/MoneyPrinterTurbo/blob/1f9f19c2021a68d04df228f33e9099a0c947f6f8/app/services/sonilo.py#L96-L132","documentation":"Raised during the Sonilo connection test when the /v1/account/services response's 'available_services' field is not a JSON list of strings. The client treats the service list as a hard protocol contract: a list whose elements include non-strings (nulls, numbers, nested objects) fails validation even if the overall payload parsed as JSON. It aborts before checking whether the video-to-music service is present.","triggerScenarios":"Calling the Sonilo connection test (which GETs /v1/account/services) where the provider returns available_services as a list of objects like [{id: video_to_music}, null], or as a plain string instead of a list. Also triggered by API version changes that rename or restructure the field into a dict of quota objects.","commonSituations":"Provider ships an undocumented API revision; a proxy or gateway returns an HTML/JSON hybrid body that deserializes into unexpected shapes; a mock or stub server used in tests returns a simplified fixture without real string entries.","solutions":["Inspect the raw response body (log response.text before parsing) and compare its available_services shape against the documented list-of-strings contract.","If the provider changed the schema, update the parsing in app/services/sonilo.py:114 to accept the new shape (for example extract service ids from a list of objects) while keeping the strict list-of-strings path.","If a proxy or gateway is mangling the response, fix or bypass the proxy so the JSON reaches the client intact.","If this came from a test stub, correct the fixture to return a list containing the string video_to_music."],"exampleFix":"// stub/fixture before\n{\"available_services\": [{\"id\": \"video_to_music\"}]}\n// after\n{\"available_services\": [\"video_to_music\"]}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"def is_valid_service_payload(payload: object) -> bool:\n    return (\n        isinstance(payload, dict)\n        and isinstance(payload.get(\"available_services\"), list)\n        and all(isinstance(s, str) for s in payload[\"available_services\"])\n    )","tryCatchPattern":"try:\n    sonilo.test_connection()\nexcept SoniloError as exc:\n    if \"invalid service list\" in str(exc):\n        # provider schema drift: inspect raw response, do not retry blindly\n        logger.error(f\"Sonilo services schema changed: {exc}\")\n    raise","preventionTips":["Pin the tested provider API version and re-run the connection test after any sonilo_base_url or provider change.","Contract-test the services endpoint in CI against a recorded fixture shaped exactly like production."],"tags":["sonilo","api-contract","validation","json"],"backgroundTag":null,"analyzedSha":"1f9f19c2021a68d04df228f33e9099a0c947f6f8","analyzedAt":"2026-08-14T19:41:05.568Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}