{"record":{"id":"9df77a4e8a0f5aae","repo":"AlistGo/alist","slug":"failed-to-decode-move-response-w","errorCode":null,"errorMessage":"failed to decode move response: %w","messagePattern":"failed to decode move response: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"drivers/proton_drive/util.go","lineNumber":722,"sourceCode":"\t}\n\n\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+d.credentials.AccessToken)\n\thttpReq.Header.Set(\"Accept\", d.protonJson)\n\thttpReq.Header.Set(\"X-Pm-Appversion\", d.webDriveAV)\n\thttpReq.Header.Set(\"X-Pm-Drive-Sdk-Version\", d.sdkVersion)\n\thttpReq.Header.Set(\"X-Pm-Uid\", d.credentials.UID)\n\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(httpReq)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute move request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tvar moveResp RenameResponse\n\tif err := json.NewDecoder(resp.Body).Decode(&moveResp); err != nil {\n\t\treturn fmt.Errorf(\"failed to decode move response: %w\", err)\n\t}\n\n\tif moveResp.Code != 1000 {\n\t\treturn fmt.Errorf(\"move operation failed with code: %d\", moveResp.Code)\n\t}\n\n\treturn nil\n}\n\nfunc (d *ProtonDrive) DirectMove(ctx context.Context, srcObj model.Obj, dstDir model.Obj) (model.Obj, error) {\n\t//fmt.Printf(\"DEBUG DirectMove: srcPath=%s, dstPath=%s\", srcObj.GetPath(), dstDir.GetPath())\n\n\tsrcLink, err := d.searchByPath(ctx, srcObj.GetPath(), srcObj.IsDir())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to find source: %w\", err)\n\t}\n\n\tvar dstParentLinkID string","sourceCodeStart":704,"sourceCodeEnd":740,"githubUrl":"https://github.com/AlistGo/alist/blob/843d9dc8149126976b2625911e45a4d3ffd6f2f5/drivers/proton_drive/util.go#L704-L740","documentation":"The move response body failed to JSON-decode. Critically, unlike the rename path, executeMoveAPI never checks resp.StatusCode before decoding — so a 401/429/500 HTML or JSON error body is decoded as if it were the success envelope, and this decode error is what surfaces, masking the real status.","triggerScenarios":"Any non-200 response whose body is HTML (proxy/Cloudflare) or an error JSON that does not fit RenameResponse; empty body from gateway errors; truncated body on flaky links.","commonSituations":"Expired token producing a 401 error JSON that the struct cannot absorb; middleboxes answering with HTML; backend 5xx pages — all misreported as 'failed to decode move response'.","solutions":["Add a status check before decoding (mirror executeRenameAPI) so the true status is reported","Log the raw body on decode failure","Fix network interception if HTML bodies appear"],"exampleFix":"// before\nvar moveResp RenameResponse\nif err := json.NewDecoder(resp.Body).Decode(&moveResp); err != nil {\n// after\nif resp.StatusCode != http.StatusOK {\n\tbody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))\n\treturn fmt.Errorf(\"move failed with status %d: %s\", resp.StatusCode, body)\n}\nvar moveResp RenameResponse\nif err := json.NewDecoder(resp.Body).Decode(&moveResp); err != nil {","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"var syn *json.SyntaxError\nvar un *json.UnmarshalTypeError\nif errors.As(err, &syn) || errors.As(err, &un) {\n    // decode failure may actually be a masked non-200 (no status check in driver):\n// check token freshness and network path, then retry once\n}","preventionTips":["Patch executeMoveAPI to check resp.StatusCode before decoding — upstream masks auth/rate-limit errors as decode errors","Refresh credentials proactively so 401 JSON never reaches the decoder"],"tags":["proton-drive","json","http","error-masking"],"backgroundTag":null,"analyzedSha":"843d9dc8149126976b2625911e45a4d3ffd6f2f5","analyzedAt":"2026-08-15T12:14:11.722Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}