chenhg5/cc-connect · error

max: send message: HTTP

Error message

max: send message: HTTP %d: %s

What it means

The MAX send-message endpoint replied with a non-200 status that was not a retryable 'attachment not ready' condition, after exhausting the backoff retry loop. The full truncated response body is included in the error.

Solutions

  1. Read the status and body in the error: 401/403 means fix the bot token in config.toml or re-add the bot to the chat; 404 means the chat is gone
  2. For 400, log the outgoing payload and validate chat_id, text length, and keyboard structure
  3. Confirm the bot access token with a lightweight /me API call
  4. Check MAX API changelog for breaking payload changes if this started after an update

Example fix

// before
return fmt.Errorf("max: send message: HTTP %d: %s", resp.StatusCode, respBody)
// after
if resp.StatusCode == http.StatusForbidden {
	return fmt.Errorf("max: send message: bot removed from chat %s (403)", chatID)
}
return fmt.Errorf("max: send message: HTTP %d: %s", resp.StatusCode, respBody)
Defensive patterns

Strategy: try-catch

Validate before calling

if err := validateOutgoing(chatID, text, keyboard); err != nil { return err } // catch 400s early

Try / catch

if err := send(); err != nil {
	if strings.Contains(err.Error(), "HTTP 403") { reauthorizeOrNotifyBotRemoved() }
	if strings.Contains(err.Error(), "HTTP 401") { alertInvalidBotToken() }
	return err
}

Prevention

When it happens

Trigger: 400 for a malformed payload (bad chat_id, oversized text, invalid keyboard JSON), 401/403 for bad or revoked bot token, 404 for unknown chat, or persistent 5xx across all retries.

Common situations: Bot removed from the chat (403) or chat deleted (404); token rotated in MAX without updating config.toml; message text exceeding MAX length limits; permanent API-side rejection of a field the platform sends.

Related errors


AI-assisted analysis of chenhg5/cc-connect@4000b2338a (2026-09-06). Data as JSON: /api/errors/9b36ae9cbe58e80f. Report an issue: GitHub.

Appendix: source

Thrown at platform/max/max.go:1416

		}
		respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
		resp.Body.Close()

		if resp.StatusCode == http.StatusOK {
			return nil
		}
		if isAttachmentNotReady(respBody) && attempt < attachmentReadyRetries {
			slog.Debug("max: attachment not ready, retrying", "attempt", attempt+1, "backoff", backoff)
			select {
			case <-ctx.Done():
				return ctx.Err()
			case <-time.After(backoff):
			}
			backoff *= 2
			continue
		}
		slog.Warn("max: send message failed", "status", resp.StatusCode, "chat", chatID, "body", string(respBody))
		return fmt.Errorf("max: send message: HTTP %d: %s", resp.StatusCode, respBody)
	}
	return fmt.Errorf("max: send message: attachment not ready after %d retries", attachmentReadyRetries)
}

func isAttachmentNotReady(body []byte) bool {
	return bytes.Contains(body, []byte("attachment.not.ready")) ||
		bytes.Contains(body, []byte("not.ready"))
}

func (p *Platform) getMe(ctx context.Context) (name string, id int64, err error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, p.apiBase+"/me", nil)
	if err != nil {
		return "", 0, err
	}
	p.setAuth(req)

	resp, err := p.client.Do(req)
	if err != nil {

View on GitHub (pinned to 4000b2338a)