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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions