skylot/jadx · error · JadxRuntimeException

Failed to save config file: {}

Error message

Failed to save config file: {}

What it means

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.

Source

Thrown at jadx-cli/src/main/java/jadx/cli/config/JadxConfigAdapter.java:83

	public @Nullable T load() {
		if (!Files.isRegularFile(configPath)) {
			// file not found
			return null;
		}
		try (JsonReader reader = gson.newJsonReader(Files.newBufferedReader(configPath))) {
			return gson.fromJson(reader, configCls);
		} catch (Exception e) {
			throw new JadxRuntimeException("Failed to load config file: " + configPath, e);
		}
	}

	public void save(T configObject) {
		try {
			String jsonStr = gson.toJson(configObject, configCls);
			// don't use stream writer here because serialization errors will corrupt config
			Files.writeString(configPath, jsonStr);
		} catch (Exception e) {
			throw new JadxRuntimeException("Failed to save config file: " + configPath, e);
		}
	}

	public String objectToJsonString(T configObject) {
		return gson.toJson(configObject, configCls);
	}

	public T jsonStringToObject(String jsonStr) {
		return gson.fromJson(jsonStr, configCls);
	}

	private Path resolveConfigRef(String configRef) {
		if (configRef == null || configRef.isEmpty()) {
			// use default config file
			return JadxCommonFiles.getConfigDir().resolve(defaultConfigFileName);
		}
		if (configRef.contains("/") || configRef.contains("\\")) {
			if (!configRef.toLowerCase().endsWith(".json")) {

View on GitHub (pinned to e738a26571)

Solutions

  1. Point the ADB forward at the real jdwp process id (`jdwp:<pid>`) rather than an arbitrary port, then retry.
  2. Verify the target APK is debuggable and that you are attaching to its jdwp thread, not a different listener.
  3. Confirm no other debugger/proxy is intercepting the forwarded port.
  4. Check that readNBytes actually returned 14 bytes; if the peer closes early, the process is not a jdwp agent.

Example fix

// before
if (buf == null || !JDWP.decodeHandShakePacket(buf)) {
    throw new SmaliDebuggerException("jdwp handshake bad reply");
}

// after (surface whether it was a short read vs wrong magic)
if (buf == null) {
    throw new SmaliDebuggerException("jdwp handshake: peer closed before sending 14 bytes (not a jdwp agent?)");
}
if (!JDWP.decodeHandShakePacket(buf)) {
    throw new SmaliDebuggerException("jdwp handshake: bad magic, first bytes=" + Arrays.toString(buf));
}
Defensive patterns

Strategy: validation

Validate before calling

// Validate the endpoint speaks jdwp by checking the handshake magic before full attach:
try (Socket s = new Socket(host, port)) {
    s.setSoTimeout(3000);
    OutputStream out = s.getOutputStream();
    out.write(JDWP.encodeHandShakePacket());
    byte[] reply = IOUtils.readNBytes(s.getInputStream(), 14);
    if (reply == null || !JDWP.decodeHandShakePacket(reply)) {
        throw new IllegalStateException(host + ":" + port + " is not a jdwp agent (bad/short handshake)");
    }
}

Try / catch

try {
    return SmaliDebugger.attach(host, port, listener);
} catch (SmaliDebuggerException e) {
    if ("jdwp handshake bad reply".equals(e.getMessage())) {
        // wrong forward target: re-point at the real jdwp pid and retry once
        ensureJdwpForwardByPid(packageName, port);
        return SmaliDebugger.attach(host, port, listener);
    }
    throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of skylot/jadx@e738a26571 (2026-08-14). Data as JSON: /api/errors/bab4d34f331b6c0b. Report an issue: GitHub.