chinabugotech/hutool · error · AIException

Failed to send POST request:

Error message

Failed to send POST request:

What it means

Thrown by BaseAIService.sendPost on a transport-level failure of HttpRequest.execute(). Hutool-http returns HTTP error statuses as a normal HttpResponse, so 4xx/5xx do NOT raise this -- only connection/socket/URL/SSL/proxy errors do. Note the message uses a full-width colon character (:, U+FF1A), not an ASCII colon, which matters if you filter or match the string. The cause is preserved.

Source

Thrown at hutool-ai/src/main/java/cn/hutool/ai/core/BaseAIService.java:94

	 * @param endpoint 请求节点
	 * @param paramJson 请求参数json
	 * @return 请求响应
	 */
	protected HttpResponse sendPost(final String endpoint, final String paramJson) {
		//链式构建请求
		try {
			HttpRequest httpRequest = HttpRequest.post(config.getApiUrl() + endpoint)
				.header(Header.CONTENT_TYPE, "application/json")
				.header(Header.ACCEPT, "application/json")
				.header(Header.AUTHORIZATION, "Bearer " + config.getApiKey())
				.body(paramJson)
				.timeout(config.getTimeout());
			if (config.getHasProxy()) {
				httpRequest.setProxy(config.getProxy());
			}
			return httpRequest.execute();
		} catch (final Exception e) {
			throw new AIException("Failed to send POST request:" + e.getMessage(), e);
		}

	}

	/**
	 * 发送表单请求
	 * @param endpoint 请求节点
	 * @param paramMap 请求参数map
	 * @return 请求响应
	 */
	protected HttpResponse sendFormData(final String endpoint, final Map<String, Object> paramMap) {
		//链式构建请求
		try {
			//设置超时3分钟
			HttpRequest httpRequest = HttpRequest.post(config.getApiUrl() + endpoint)
				.header(Header.CONTENT_TYPE, "multipart/form-data")
				.header(Header.ACCEPT, "application/json")
				.header(Header.AUTHORIZATION, "Bearer " + config.getApiKey())

View on GitHub (pinned to 8870454b2a)

Solutions

  1. Confirm config.getApiUrl() + endpoint resolves and accepts POST.
  2. Raise setTimeout/setReadTimeout for slow generation.
  3. Inspect ex.getCause() for the underlying IOException.
  4. If matching this message in logs, use the full-width colon (:) literally.
  5. For HTTP business errors, read response.getStatus()/body() on the success path instead.

Example fix

// before
HttpResponse resp = service.sendPost("/v1/chat/completions", json);
// transport fails -> Failed to send POST request:...

// after
AIConfig cfg = new AIConfigBuilder(ModelName.OPENAI.getValue())
    .setApiKey(k).setApiUrl("https://api.openai.com")
    .setTimeout(60_000).setReadTimeout(120_000).build();
HttpResponse resp = service.sendPost("/v1/chat/completions", json);
if (!resp.isOk()) { /* handle 4xx/5xx body */ }
Defensive patterns

Strategy: try-catch

Validate before calling

// Validate URL + timeout before the POST
String base = config.getApiUrl();
if (StrUtil.isBlank(base)) throw new IllegalStateException("apiUrl blank");
if (config.getTimeout() < 10_000) config.setTimeout(30_000);

Try / catch

try {
    return service.sendPost(endpoint, json);
} catch (AIException e) {
    // NOTE: message uses full-width colon (:)
    if (e.getMessage().contains("Failed to send POST request")) {
        Throwable root = e.getCause(); // real IOException
        // transport failure only; HTTP 4xx/5xx are returned, not thrown
    }
    throw e;
}

Prevention

When it happens

Trigger: POST to an unreachable/malformed apiUrl, connect/read timeout, SSL handshake failure, or proxy failure during chat/embeddings calls on models inheriting BaseAIService.sendPost (deepseek, openai, doubao, grok, hutool). A large or malformed paramJson causes a different error path, not this one.

Common situations: Wrong base URL; network outage; timeout too low; reverse-proxy rejecting the path; self-signed cert. Invalid apiKey or rate-limit returns an HTTP status (handled by the caller), not this exception.

Related errors


AI-assisted analysis of chinabugotech/hutool@8870454b2a (2026-08-14). Data as JSON: /api/errors/0e07622cfa84a502. Report an issue: GitHub.