{"record":{"id":"0ce5db59d2587a66","repo":"jeecgboot/JeecgBoot","slug":"tts-api-statuscode-errorbody","errorCode":null,"errorMessage":"TTS API调用失败，状态码: {statusCode}，{errorBody}","messagePattern":"TTS API调用失败，状态码: (.+?)，(.+?)","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"jeecg-boot/jeecg-boot-module/jeecg-boot-module-airag/src/main/java/org/jeecg/modules/airag/voice/util/VoiceApiHelper.java","lineNumber":83,"sourceCode":"        body.put(\"voice\", voice);\n        body.put(\"speed\", speed);\n        body.put(\"response_format\", \"wav\");\n\n        log.info(\"TTS请求: url={}, model={}, voice={}, speed={}, textLength={}\", url, config.getModel(), voice, speed, text.length());\n\n        HttpRequest request = HttpRequest.newBuilder()\n                .uri(URI.create(url))\n                .header(\"Authorization\", \"Bearer \" + config.getApiKey())\n                .header(\"Content-Type\", \"application/json\")\n                .POST(HttpRequest.BodyPublishers.ofString(body.toJSONString()))\n                .timeout(Duration.ofSeconds(config.getTimeout()))\n                .build();\n\n        HttpResponse<InputStream> response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());\n        if (response.statusCode() != 200) {\n            String errorBody = new String(response.body().readAllBytes());\n            log.error(\"TTS API调用失败: status={}, body={}\", response.statusCode(), errorBody);\n            throw new RuntimeException(\"TTS API调用失败，状态码: \" + response.statusCode() + \"，\" + errorBody);\n        }\n\n        try (InputStream is = response.body()) {\n            Files.copy(is, audioPath, StandardCopyOption.REPLACE_EXISTING);\n        }\n        log.info(\"TTS语音已生成: {}\", audioPath);\n    }\n}\n","sourceCodeStart":65,"sourceCodeEnd":92,"githubUrl":"https://github.com/jeecgboot/JeecgBoot/blob/96fb33f5ec68516da0b0147da06b2eb0419e063a/jeecg-boot/jeecg-boot-module/jeecg-boot-module-airag/src/main/java/org/jeecg/modules/airag/voice/util/VoiceApiHelper.java#L65-L92","documentation":"VoiceApiHelper.generateAudio() POSTs an OpenAI-compatible request to {apiHost}/audio/speech. Any HTTP status other than 200 is thrown as a RuntimeException that embeds both the status code and the raw upstream error body. The request uses Bearer apiKey auth with a configurable timeout.","triggerScenarios":"401 = apiKey invalid/revoked; 403/region blocked; 404 = apiHost or model wrong; 429 = rate/quota exceeded; 400 = unsupported voice/model/response_format; 5xx = provider outage. Also a missing/blank apiHost produces a malformed URL.","commonSituations":"apiKey expired or copied with whitespace; apiHost missing trailing handling mismatch; model name not provisioned on the account; provider rate limiting under load; apiHost pointing at a non-OpenAI base that lacks /audio/speech.","solutions":["Read the status code and errorBody baked into the message -- they identify the failure class.","401/403 -> correct/regenerate the apiKey in aiChatConfig.aiModelVoice.","429 -> add backoff/throttling or raise provider quota; retry with jitter.","404/400 -> verify apiHost base URL and that model + voice exist on the provider.","Confirm the provider actually exposes the /audio/speech endpoint."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// before calling VoiceApiHelper, sanity-check config\nAiChatConfig.VoiceModelConfig c = aiChatConfig.getAiModelVoice();\nif (oConvertUtils.isEmpty(c.getApiHost()) || oConvertUtils.isEmpty(c.getApiKey())) {\n    throw new IllegalStateException(\"TTS apiHost/apiKey 未配置\");\n}","typeGuard":null,"tryCatchPattern":"// retry only transient (5xx/429) failures; fail fast on 4xx auth errors\ntry {\n    voiceApiHelper.generateAudio(text, audioPath, voice, speed);\n} catch (RuntimeException e) {\n    String msg = e.getMessage();\n    if (msg.contains(\"状态码: 429\") || msg.contains(\"状态码: 5\")) {\n        // backoff and retry once\n    } else {\n        throw e;\n    }\n}","preventionTips":["Store apiKey via a secrets manager, never in plain yml.","Validate apiHost ends with '/' handling the url-join logic already in place.","Add a small test request at deploy time to confirm 200."],"tags":["tts","http","api","auth","network","rate-limit"],"backgroundTag":null,"analyzedSha":"96fb33f5ec68516da0b0147da06b2eb0419e063a","analyzedAt":"2026-08-14T00:04:16.786Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}