{"record":{"id":"761ed46728c19fd1","repo":"chenhg5/cc-connect","slug":"stream-ai-card-status-d-body-s","errorCode":null,"errorMessage":"stream AI card: status=%d, body=%s","messagePattern":"stream AI card: status=(.+?), body=(.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"platform/dingtalk/card.go","lineNumber":377,"sourceCode":"\n\tslog.Debug(\"dingtalk: streaming response\",\n\t\t\"status\", resp.StatusCode,\n\t\t\"body\", string(respBody),\n\t\t\"isFinalize\", isFinalize)\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tslog.Error(\"dingtalk: stream AI card failed\",\n\t\t\t\"status\", resp.StatusCode,\n\t\t\t\"body\", string(respBody))\n\t\t// Check if we should trigger degrade\n\t\tif resp.StatusCode == 403 || resp.StatusCode == 429 || resp.StatusCode >= 500 {\n\t\t\tc.platform.activateCardDegrade(fmt.Sprintf(\"card.stream:%d\", resp.StatusCode))\n\t\t\tc.mu.Lock()\n\t\t\tc.state = \"failed\"\n\t\t\tclose(c.done)\n\t\t\tc.mu.Unlock()\n\t\t}\n\t\treturn fmt.Errorf(\"stream AI card: status=%d, body=%s\", resp.StatusCode, string(respBody))\n\t}\n\n\tslog.Debug(\"dingtalk: AI card streamed successfully\", \"isFinalize\", isFinalize)\n\treturn nil\n}\n\n// Finalize sends the final content and marks the card as complete.\nfunc (c *aiCard) Finalize(ctx context.Context, content string) error {\n\tc.mu.Lock()\n\n\t// Stop any pending timer\n\tif c.timer != nil {\n\t\tc.timer.Stop()\n\t\tc.timer = nil\n\t}\n\n\t// If already finished or failed, skip\n\tif c.state == \"finished\" || c.state == \"failed\" {","sourceCodeStart":359,"sourceCodeEnd":395,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/platform/dingtalk/card.go#L359-L395","documentation":"DingTalk's /v1.0/card/streaming endpoint returned a non-200 status for a card content update (platform/dingtalk/card.go:377, doStream). The error includes the status code and response body. For statuses 403, 429, and >=500, the card is additionally marked failed (state=\"failed\", done channel closed) and activateCardDegrade switches the platform to plain-text replies.","triggerScenarios":"PUT /v1.0/card/streaming returns non-200: expired/invalid access token (401), streaming permission denied or card already finalized (403), unknown outTrackId / card instance no longer exists (400/404), QPS throttling (429), or DingTalk server errors (5xx).","commonSituations":"Streaming continues after the token expired mid-card (401); card was deleted or conversation disbanded while streaming (404); sending final updates after the card was already finalized elsewhere; exceeding API QPS with aggressive throttle settings (429); DingTalk incident (5xx).","solutions":["Read the embedded status and body — DingTalk returns a JSON code identifying the cause","On 401: ensure getAccessToken refreshes expired tokens; verify appKey/appSecret are correct","On 429: increase cardThrottleMs to lower streaming request rate; degrade mode activates automatically","On 404/400: the card instance is gone — abandon the card and send the content as a normal text message instead","On 5xx: retry; once the card state is \"failed\", Update/Finalize become no-ops, so fall back to a plain reply","Check the DingTalk open platform console for card template and streaming-API permission issues on 403"],"exampleFix":"// before\ncardThrottleMs = 200 // may exceed QPS limits\n// after\ncardThrottleMs = 1000 // reduce streaming request rate to avoid 429","handlingStrategy":"fallback","validationCode":"// verify token validity and template key before streaming\nif _, err := c.platform.getAccessToken(context.Background()); err != nil {\n\treturn fmt.Errorf(\"skip streaming; token invalid: %w\", err)\n}\nif c.templateKey == \"\" {\n\treturn fmt.Errorf(\"cardTemplateKey not configured; streaming updates will be rejected\")\n}","typeGuard":null,"tryCatchPattern":"if err := card.Finalize(ctx, content); err != nil && strings.Contains(err.Error(), \"status=\") {\n\tswitch {\n\tcase strings.Contains(err.Error(), \"status=401\"):\n\t\t// refresh token and retry once\n\tcase strings.Contains(err.Error(), \"status=429\"):\n\t\t// back off; raise throttle interval\n\tdefault:\n\t\t// card may be failed/gone: send plain text message instead\n\t\tp.Reply(ctx, msg, content)\n\t}\n}","preventionTips":["Set cardThrottleMs high enough to stay under DingTalk QPS limits","Refresh access tokens before expiry so long streams don't hit 401","Don't finalize or delete cards concurrently with streaming updates","Handle card failure (state=failed) by switching to plain-text replies for the remainder of the session"],"tags":["http","api","dingtalk","streaming","card","http-error"],"backgroundTag":"http-error-response","analyzedSha":"4000b2338aa6e850c99df54f8b0ed6ed7460b401","analyzedAt":"2026-09-06T11:45:09.575Z","contentChangedAt":"2026-09-06T11:45:09.575Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}