skylot/jadx · error · JadxRuntimeException

Class decompilation failed

Error message

Class decompilation failed

What it means

Thrown by SmaliDebugger.initJDWP after the handshake and command exchange completed without IOException, but the reply packets did not satisfy the expected JDWP contract: either the Suspend reply or the IDSizes reply was not a reply packet or did not carry packet id 1. Reaching the trailing throw means the peer answered but its answers were not the expected replies, so the JDWP layer could not be initialized and field sizes remain unknown.

Source

Thrown at jadx-cli/src/main/java/jadx/cli/SingleClassMode.java:63

			}
		} else {
			// singleClassOutput is set
			// expect only one class to be loaded
			List<ClassNode> classes = jadx.getRoot().getClasses().stream()
					.filter(c -> !c.isInner() && !c.contains(AFlag.DONT_GENERATE))
					.collect(Collectors.toList());
			int size = classes.size();
			if (size == 1) {
				clsForProcess = classes.get(0);
			} else {
				throw new JadxArgsValidateException("Found " + size + " classes, single class output can't be used");
			}
		}
		ICodeInfo codeInfo;
		try {
			codeInfo = clsForProcess.decompile();
		} catch (Exception e) {
			throw new JadxRuntimeException("Class decompilation failed", e);
		}
		String fileExt = SaveCode.getFileExtension(jadx.getRoot());
		File out;
		if (singleClassOutput == null) {
			out = new File(jadx.getArgs().getOutDirSrc(), clsForProcess.getClassInfo().getAliasFullPath() + fileExt);
		} else {
			if (singleClassOutput.endsWith(fileExt)) {
				// treat as file name
				out = new File(singleClassOutput);
			} else {
				// treat as directory
				out = new File(singleClassOutput, clsForProcess.getShortName() + fileExt);
			}
		}
		File resultOut = FileUtils.prepareFile(out);
		if (clsForProcess.getClassInfo().hasAlias()) {
			LOG.info("Saving class '{}' (alias: '{}') to file '{}'",
					clsForProcess.getClassInfo().getFullName(), clsForProcess.getFullName(), resultOut.getAbsolutePath());

View on GitHub (pinned to e738a26571)

Solutions

  1. Recreate the ADB jdwp forward against the correct process id and ensure nothing else is attached.
  2. Confirm the target is an Android debuggable process exposing jdwp (not an arbitrary TCP service).
  3. Restart the target app so its jdwp thread is fresh, then re-attach before any other client does.
  4. Upgrade jadx/jdwp library; if reproducible on a stock device, report the unexpected reply as a protocol bug with the packet dump.

Example fix

// before
if (res.isReplyPacket() && res.getID() == 1) {
    JDWP.IDSizes.IDSizesReplyData sizes = JDWP.IDSizes.decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE);
    return new JDWP(sizes);
}
// falls through to:
throw new SmaliDebuggerException("Failed to init JDWP.");

// after (diagnose instead of opaque failure)
if (!res.isReplyPacket() || res.getID() != 1) {
    throw new SmaliDebuggerException("JDWP init: expected reply id 1, got id=" + res.getID()
        + " reply=" + res.isReplyPacket() + " (peer is not a conformant jdwp agent?)");
}
Defensive patterns

Strategy: retry

Validate before calling

// Confirm the forward target is a jdwp endpoint (responds to handshake) before full init:
if (!probeJdwpHandshake(host, port)) {
    throw new IllegalStateException("Port " + port + " is not a conformant jdwp endpoint");
}
// only then call SmaliDebugger.attach which runs initJDWP

Try / catch

// initJDWP is private and called from attach; guard at the attach boundary
try {
    return SmaliDebugger.attach(host, port, listener);
} catch (SmaliDebuggerException e) {
    if ("Failed to init JDWP.".equals(e.getMessage())) {
        // non-conformant or stale endpoint: refresh forward + restart target, then retry once
        restartDebuggableApp(packageName);
        ensureJdwpForward(packageName, port);
        return SmaliDebugger.attach(host, port, listener);
    }
    throw e;
}

Prevention

When it happens

Trigger: initJDWP runs handshake, sends Suspend (packet id 1) then IDSizes (packet id 1), checking res.isReplyPacket() && res.getID() == 1 at each step. If either check fails the code falls through past the nested ifs to the final throw. Happens when the connected endpoint speaks a non-JDWP or non-conformant protocol, or when packets arrive out of order / with unexpected ids.

Common situations: The ADB forward target is not actually a jdwp endpoint (e.g. forwarded to a plain app port); a non-Android JVM or a custom agent that doesn't honor the JDWP suspend/IDSizes command/reply pairing; packet id mismatch from a half-initiated session; race with another client consuming the replies.

Related errors


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