Skip to content

Non-JSON server response in Transport.processResponse() causes permanent networking deadlock via uncaught JSONException #264

@DanBoSlice

Description

@DanBoSlice

When the Countly server returns a non-JSON response (e.g., an HTML page from a 502 Bad Gateway or 503 Service Unavailable), Transport.processResponse() throws a JSONException that is not caught by Transport.send(). This uncaught exception prevents Tasks.running from being reset to null, causing Tasks.isRunning() to return true permanently. As a result, DefaultNetworking.check() never submits new send tasks, and the entire networking queue is deadlocked until the SDK is reinitialized.

Root Cause

Transport.processResponse() unconditionally parses the response body as JSON:

// Transport.java — processResponse()
JSONObject json = new JSONObject(response);  // throws JSONException on HTML/non-JSON

Transport.send() catches only IOException:

// Transport.java — send()
try {
    // ... HTTP request and processResponse() call
} catch (IOException e) {
    // handles network errors
}
// JSONException (extends RuntimeException) is NOT caught

The exception propagates up to Tasks, where the executor wrapper has:

// Tasks.java — task execution
running = task.id;    // ← set before execution
task.run();           // ← exception thrown here
running = null;       // ← NEVER reached (not in a finally block)

Since running is never reset, Tasks.isRunning() returns true forever, and DefaultNetworking.check() skips submitting new tasks.

Steps to Reproduce

  1. Initialize the Countly SDK
  2. Queue some events/requests
  3. Have the Countly server return a non-JSON response (e.g., a reverse proxy returning 502/503 HTML)
  4. Observe that isSending() returns true permanently and no further requests are ever sent

Expected Behavior

  1. Transport.send() should catch RuntimeException (or at minimum JSONException) in addition to IOException, treating non-JSON responses as a transient failure with retry.
  2. Tasks should reset running = null in a finally block to ensure the executor never gets permanently stuck regardless of exception type.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions