Skip to content

Uncaught NPE in siteResponseToSiteModel when WP.com returns blogging_prompts_settings with null reminders_days/reminders_time #23014

Description

@jkmassel

Summary

  • SiteRestClient.siteResponseToSiteModel() parses blogging_prompts_settings from the WP.com sites response and dereferences two sub-fields without a null guard. If the server returns blogging_prompts_settings as a non-null object but with reminders_days or reminders_time null/absent, the parse throws an uncaught NullPointerException and the entire site fetch fails.
  • Both fields are plain unannotated Java fields (Kotlin platform types), so the compiler inserts no caller-side null check — same mechanism as the null-URL crash currently live in production (Sentry JETPACK-ANDROID-1JAS).
  • Latent today — no production occurrences yet — but reachable: blogging_prompts_settings ships under options, which is in the fields= allowlist, and there is zero test coverage of this path.

Root cause

In libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/site/SiteRestClient.kt, inside siteResponseToSiteModel() (the from.options.blogging_prompts_settings?.let { ... } block, ~L1131–1150). The ?.let guards the parent object being null; it does nothing for the sub-fields:

1. reminders_days — null Map dereference (~L1137–1143) ❌

site.setIsBloggingReminderOnMonday(it.reminders_days["monday"] ?: false)
// ... through Sunday

it.reminders_days["monday"] desugars to it.reminders_days.get("monday"). The ?: false operates on the return value of get, not the receiver — so a null reminders_days map NPEs on the .get call. These seven lines are not inside any try/catch.

reminders_days is declared public Map<String, Boolean> reminders_days; (SiteWPComRestResponse.java:107) — no annotation, no @SerializedName, no custom deserializer. Stock Gson leaves it null when the key is absent or its value is JSON null.

2. reminders_time — null String dereference (~L1144–1149) ❌

try {
    site.bloggingReminderHour = it.reminders_time.split(".")[0].toInt()
    site.bloggingReminderMinute = it.reminders_time.split(".")[1].toInt()
} catch (ex: NumberFormatException) {
    AppLog.e(API, "Received malformed blogging reminder time: " + ex.message)
}

it.reminders_time.split(".") throws NPE on a null reminders_time. The surrounding try catches only NumberFormatExceptionNullPointerException is not a subclass of it (NFE → IllegalArgumentException; NPE → RuntimeException), so the NPE escapes the catch. reminders_time is public String reminders_time; (SiteWPComRestResponse.java:108), same platform-type/Gson story.

The crash propagates uncaught out of siteResponseToSiteModel() to its callers (fetchSite, fetchSites, and the site-feature path).

Why it can happen

The "the server always sends a complete blogging_prompts_settings object" assumption is unverified and unenforced — no @NonNull, no test, no fixture. We have direct, recent production proof that this exact endpoint violates field-presence assumptions: site.url = from.URL reads an identically-unannotated field and just started returning null in production (Sentry JETPACK-ANDROID-1JAS, ~1,300 crashes / ~180 users in the first ~7.5h). The method itself already distrusts this object's contents (the NumberFormatException catch on reminders_time) — it just doesn't guard the fields' presence. /me/sites/ and /sites/$site/ are also documented in-method as returning different shapes for the same fields.

Evidence

Adversarially validated by three independent reviewers; two reproduced the exact exceptions with a standalone kotlinc repro of the platform-type + deref pattern:

  • reminders_days null → java.lang.NullPointerException: Cannot invoke "java.util.Map.get(Object)" because "it.reminders_days" is null
  • reminders_time null → java.lang.NullPointerException: reminders_time must not be null (Kotlin Intrinsics.checkNotNullExpressionValue emitted before split)

Bytecode confirmed the ?: false Elvis sits on the get result, after the null receiver has already been invoked.

Fix

Null-safe both dereferences (the surrounding code already treats this object as untrusted):

// reminders_days — all seven day lines:
site.setIsBloggingReminderOnMonday(it.reminders_days?.get("monday") ?: false)

// reminders_time — safe-call before split (or broaden the catch to cover NPE/IndexOutOfBounds):
it.reminders_time?.split(".")?.let { parts ->
    try {
        site.bloggingReminderHour = parts[0].toInt()
        site.bloggingReminderMinute = parts[1].toInt()
    } catch (ex: NumberFormatException) {
        AppLog.e(API, "Received malformed blogging reminder time: " + ex.message)
    }
}

A regression test that feeds siteResponseToSiteModel a blogging_prompts_settings with each sub-field null would lock this in — the path currently has no coverage.

Related

  • Live null-URL incident (same root pattern, different field): Sentry JETPACK-ANDROID-1JAS. The URL case is being addressed server-side (it's the only field that crashes the java.net.URI parser); these two are the remaining client-side null-deref siblings in the same parse method.

Found while triaging the null-URL crash.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions