{"record":{"id":"79df759764f62813","repo":"chenhg5/cc-connect","slug":"upload-url-empty-url-in-response","errorCode":null,"errorMessage":"upload url: empty url in response","messagePattern":"upload url: empty url in response","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"platform/max/max.go","lineNumber":568,"sourceCode":"\n\turlResp, err := p.uploadClient.Do(urlReq)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"request upload url: %w\", err)\n\t}\n\tdefer urlResp.Body.Close()\n\tif urlResp.StatusCode != http.StatusOK {\n\t\tbody, _ := io.ReadAll(io.LimitReader(urlResp.Body, 512))\n\t\treturn \"\", fmt.Errorf(\"upload url: HTTP %d: %s\", urlResp.StatusCode, body)\n\t}\n\tvar urlInfo struct {\n\t\tURL   string `json:\"url\"`\n\t\tToken string `json:\"token\"`\n\t}\n\tif err := json.NewDecoder(urlResp.Body).Decode(&urlInfo); err != nil {\n\t\treturn \"\", fmt.Errorf(\"decode upload url: %w\", err)\n\t}\n\tif urlInfo.URL == \"\" {\n\t\treturn \"\", fmt.Errorf(\"upload url: empty url in response\")\n\t}\n\n\tif filename == \"\" {\n\t\tfilename = defaultFilename(kind)\n\t}\n\tvar buf bytes.Buffer\n\tmw := multipart.NewWriter(&buf)\n\tfw, err := mw.CreateFormFile(\"data\", filename)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif _, err := fw.Write(data); err != nil {\n\t\treturn \"\", err\n\t}\n\tif err := mw.Close(); err != nil {\n\t\treturn \"\", err\n\t}\n","sourceCodeStart":550,"sourceCodeEnd":586,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/platform/max/max.go#L550-L586","documentation":"uploadAttachment in platform/max/max.go performs MAX's two-step upload: it first POSTs to /uploads?type=<kind> to obtain a presigned CDN upload URL plus token. If the API answers 200 with valid JSON but the decoded \"url\" field is an empty string, the platform cannot proceed with the CDN POST and throws this error. It signals that MAX's upload-URL endpoint returned a well-formed but incomplete response.","triggerScenarios":"Calling SendImage/SendFile/SendAudio when the MAX API returns HTTP 200 on POST /uploads with JSON that decodes successfully but lacks a non-empty \"url\" field (e.g. {\"token\":\"...\"} only, or {}), such as when MAX changes its upload-URL response schema or a proxy strips the field.","commonSituations":"MAX server-side API change or deprecation of the /uploads flow; a corporate proxy/gateway that mangles or rewrites the response body; testing against a stub/mock server that returns an incomplete upload-URL payload; expired or mis-scoped bot token causing MAX to return 200 with a degraded body instead of an error.","solutions":["Log the raw /uploads response body (before decoding) to confirm what MAX actually returned and compare against the documented {\"url\":\"...\",\"token\":\"...\"} shape.","Verify the bot token is valid and has upload/attachment permissions; re-test with a fresh token via curl -X POST 'https://botapi.max.ru/uploads?type=image' -H 'Authorization: ...'.","Check the kind query parameter: only image/video/audio/file are supported; an unexpected type can make MAX return a body without a url.","If MAX changed the response schema, update the urlInfo struct tags in platform/max/max.go (line 560-563) to match the new field name.","Retry once — if transient upstream degradation, a repeat request may return a proper URL."],"exampleFix":"// before (blind failure)\nif urlInfo.URL == \"\" {\n\treturn \"\", fmt.Errorf(\"upload url: empty url in response\")\n}\n// after (log the raw body for diagnosability)\nraw, _ := io.ReadAll(io.LimitReader(urlResp.Body, 4096))\nvar urlInfo struct {\n\tURL   string `json:\"url\"`\n\tToken string `json:\"token\"`\n}\nif err := json.Unmarshal(raw, &urlInfo); err != nil {\n\treturn \"\", fmt.Errorf(\"decode upload url: %w\", err)\n}\nif urlInfo.URL == \"\" {\n\treturn \"\", fmt.Errorf(\"upload url: empty url in response (body=%s)\", raw)\n}","handlingStrategy":"validation","validationCode":"// Pre-check before uploading: ensure non-empty payload and a supported kind\nfunc validateUpload(kind string, data []byte) error {\n\tif len(data) == 0 { return fmt.Errorf(\"empty attachment data\") }\n\tswitch kind {\n\tcase \"image\", \"video\", \"audio\", \"file\":\n\t\treturn nil\n\t}\n\treturn fmt.Errorf(\"unsupported upload kind: %q\", kind)\n}","typeGuard":"func hasUploadURL(resp *struct{ URL string `json:\"url\"`; Token string `json:\"token\"` }) bool {\n\treturn resp != nil && resp.URL != \"\"\n}","tryCatchPattern":"token, err := p.uploadAttachment(ctx, kind, data, filename)\nif err != nil {\n\tif strings.Contains(err.Error(), \"empty url in response\") {\n\t\tslog.Warn(\"max: /uploads returned no url; check token/API version\", \"kind\", kind)\n\t\treturn fmt.Errorf(\"max: upload unavailable: %w\", err)\n\t}\n\treturn err\n}","preventionTips":["Pin and monitor the MAX Bot API version; test /uploads in CI against the live API.","Log the raw /uploads response body whenever the decoded url is empty.","Keep the bot token valid and scoped for uploads; rotate before expiry.","Only pass the documented kinds (image/video/audio/file) as ?type=."],"tags":["http","api","upload","max"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"4000b2338aa6e850c99df54f8b0ed6ed7460b401","analyzedAt":"2026-09-06T11:45:09.575Z","contentChangedAt":"2026-09-06T11:45:09.575Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}