{"record":{"id":"bab4d34f331b6c0b","repo":"skylot/jadx","slug":"failed-to-save-config-file","errorCode":null,"errorMessage":"Failed to save config file: {}","messagePattern":"Failed to save config file: (.+?)","errorType":"exception","errorClass":"JadxRuntimeException","httpStatus":null,"severity":"error","filePath":"jadx-cli/src/main/java/jadx/cli/config/JadxConfigAdapter.java","lineNumber":83,"sourceCode":"\tpublic @Nullable T load() {\n\t\tif (!Files.isRegularFile(configPath)) {\n\t\t\t// file not found\n\t\t\treturn null;\n\t\t}\n\t\ttry (JsonReader reader = gson.newJsonReader(Files.newBufferedReader(configPath))) {\n\t\t\treturn gson.fromJson(reader, configCls);\n\t\t} catch (Exception e) {\n\t\t\tthrow new JadxRuntimeException(\"Failed to load config file: \" + configPath, e);\n\t\t}\n\t}\n\n\tpublic void save(T configObject) {\n\t\ttry {\n\t\t\tString jsonStr = gson.toJson(configObject, configCls);\n\t\t\t// don't use stream writer here because serialization errors will corrupt config\n\t\t\tFiles.writeString(configPath, jsonStr);\n\t\t} catch (Exception e) {\n\t\t\tthrow new JadxRuntimeException(\"Failed to save config file: \" + configPath, e);\n\t\t}\n\t}\n\n\tpublic String objectToJsonString(T configObject) {\n\t\treturn gson.toJson(configObject, configCls);\n\t}\n\n\tpublic T jsonStringToObject(String jsonStr) {\n\t\treturn gson.fromJson(jsonStr, configCls);\n\t}\n\n\tprivate Path resolveConfigRef(String configRef) {\n\t\tif (configRef == null || configRef.isEmpty()) {\n\t\t\t// use default config file\n\t\t\treturn JadxCommonFiles.getConfigDir().resolve(defaultConfigFileName);\n\t\t}\n\t\tif (configRef.contains(\"/\") || configRef.contains(\"\\\\\")) {\n\t\t\tif (!configRef.toLowerCase().endsWith(\".json\")) {","sourceCodeStart":65,"sourceCodeEnd":101,"githubUrl":"https://github.com/skylot/jadx/blob/e738a26571d02919f01df40de93bc9a44dee4e18/jadx-cli/src/main/java/jadx/cli/config/JadxConfigAdapter.java#L65-L101","documentation":"Thrown by SmaliDebugger.handShake when the 14-byte reply was read without exception but is null (short read) or does not equal the JDWP magic handshake bytes (JDWP.decodeHandShakePacket(buf) is false). The TCP peer answered but its answer is not the JDWP handshake string, meaning the connected endpoint is not a JDWP agent even though the socket itself is healthy.","triggerScenarios":"After a clean write/read, buf is null or fails the magic check. Happens when the ADB forward points at a port serving something other than jdwp (e.g. an HTTP/proprietary service), when the first 14 bytes are garbage due to stream misalignment, or when readNBytes returns fewer than 14 bytes (null) because the peer closed early.","commonSituations":"Forwarded the wrong local port; the target is not a debuggable Android process; connected to the app's own listener instead of its jdwp thread; a man-in-the-middle/proxy rewriting the stream; partial data because the peer closed the socket after <14 bytes.","solutions":["Point the ADB forward at the real jdwp process id (`jdwp:<pid>`) rather than an arbitrary port, then retry.","Verify the target APK is debuggable and that you are attaching to its jdwp thread, not a different listener.","Confirm no other debugger/proxy is intercepting the forwarded port.","Check that readNBytes actually returned 14 bytes; if the peer closes early, the process is not a jdwp agent."],"exampleFix":"// before\nif (buf == null || !JDWP.decodeHandShakePacket(buf)) {\n    throw new SmaliDebuggerException(\"jdwp handshake bad reply\");\n}\n\n// after (surface whether it was a short read vs wrong magic)\nif (buf == null) {\n    throw new SmaliDebuggerException(\"jdwp handshake: peer closed before sending 14 bytes (not a jdwp agent?)\");\n}\nif (!JDWP.decodeHandShakePacket(buf)) {\n    throw new SmaliDebuggerException(\"jdwp handshake: bad magic, first bytes=\" + Arrays.toString(buf));\n}","handlingStrategy":"validation","validationCode":"// Validate the endpoint speaks jdwp by checking the handshake magic before full attach:\ntry (Socket s = new Socket(host, port)) {\n    s.setSoTimeout(3000);\n    OutputStream out = s.getOutputStream();\n    out.write(JDWP.encodeHandShakePacket());\n    byte[] reply = IOUtils.readNBytes(s.getInputStream(), 14);\n    if (reply == null || !JDWP.decodeHandShakePacket(reply)) {\n        throw new IllegalStateException(host + \":\" + port + \" is not a jdwp agent (bad/short handshake)\");\n    }\n}","typeGuard":null,"tryCatchPattern":"try {\n    return SmaliDebugger.attach(host, port, listener);\n} catch (SmaliDebuggerException e) {\n    if (\"jdwp handshake bad reply\".equals(e.getMessage())) {\n        // wrong forward target: re-point at the real jdwp pid and retry once\n        ensureJdwpForwardByPid(packageName, port);\n        return SmaliDebugger.attach(host, port, listener);\n    }\n    throw e;\n}","preventionTips":["Forward to jdwp:<pid>, not to an arbitrary app port or local listener.","Attach to a debuggable Android process only.","Ensure no proxy/other client rewrites the forwarded stream.","Confirm readNBytes returns the full 14 bytes; an early close means the peer is not jdwp."],"tags":["debugger","jdwp","handshake","network","adb"],"backgroundTag":null,"analyzedSha":"e738a26571d02919f01df40de93bc9a44dee4e18","analyzedAt":"2026-08-14T00:10:24.238Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}