Skip to content

App crashes on launch / during Nightscout sync after Nocturne became the writer of Nightscout data (instead of Trio directly) #793

Closed

Description

@markjoldham

Summary

LoopFollow crashes shortly after launch (and again during normal operation) when its Nightscout backend is fed documents in a full, fixed-schema style — every field always present, with JSON null for anything unused — instead of the traditional sparse style where unused fields are simply omitted from the document.

I hit this after reconfiguring how my data flows between Trio, Nocturne, and Nightscout:

  • Before: Trio → Nightscout (Trio uploaded directly to Nightscout), and Nightscout → Nocturne (Nocturne only read/consumed from Nightscout, it did not write to it).
  • After: Trio → Nocturne (Trio now sends to Nocturne instead), and Nocturne → Nightscout (Nocturne now writes to Nightscout). So Nightscout's devicestatus/treatments documents are now authored by Nocturne instead of by Trio directly.

I don't know the exact mechanism Nocturne uses to write to Nightscout (whether it goes through the normal Nightscout REST API or some other path) — what's confirmed below is the resulting change in document shape, verified directly against my own Nightscout data. Within 24–48 hours of this change, the app began crashing on launch, and after an initial fix, crashing again during normal treatment-graph updates.

Environment

  • LoopFollow: two separate crash reports — an initial crash, and a second, distinct crash that surfaced only after the first was fixed
  • Nightscout: self-hosted (Heroku), MongoDB-backed
  • AID: Trio → Nocturne → Nightscout (previously Trio → Nightscout directly; reconfigured ~24–48h before symptoms started; Nocturne's write mechanism not confirmed)

Root cause

The two uploaders encode "this field has no value" completely differently:

Classic / sparse uploader Nocturne / full-schema uploader
Unused field Key omitted entirely Key present, value null
devicestatus.loop key does not exist "loop": null
treatments[].timestamp key does not exist (only created_at present) "timestamp": null (plus created_at)

LoopFollow's Nightscout-parsing code was written assuming the first style: a key is either present with real data, or it's absent. In several places the code does:

if dict["key"] != nil {
    let value = dict["key"] as! SomeType   // force cast
}

When JSONSerialization decodes a JSON null, it produces NSNull — a real, non-nil object — not Swift's nil. So dict["key"] != nil is true for an explicit null, and the subsequent as! force-cast to a concrete type crashes with a Swift runtime fatal error (swift_dynamicCastFailure → abort()), because NSNull cannot be cast to String, [String: AnyObject], etc.

This was never hit before because the old uploader never sent those keys at all when they had no value, so the != nil check correctly returned false and the force-cast was never reached.

Evidence from live data

Pulled directly from my Nightscout instance (read-only token), comparing records from before and after the uploader switch:

devicestatus — old uploader (≈1 month ago), sparse:

{
  "_id": "...", "created_at": "...", "device": "...",
  "openaps": { ... }, "pump": { ... }, "uploader": { ... }, "utcOffset": 0
}

7 top-level keys. No loop key at all.

devicestatus — new uploader (current), full schema:

{
  "_id": "...", "cgm": null, "connect": null, "createdAt": "...",
  "created_at": "...", "device": "Trio", "id": null, "insulinPen": null,
  "isCharging": null, "loop": null, "meter": null, "mills": ...,
  "mmtune": null, "openaps": { ... }, "override": null, "pump": { ... },
  "radioAdapter": null, "srvCreated": ..., "srvModified": ...,
  "uploader": { ... }, "uploaderBattery": ..., "utcOffset": 0, "xdripjs": null
}

22 top-level keys, 11 of them explicit null, including "loop": null — this key is now always present.

I binary-searched the transition and found it's an instantaneous cutover, not a gradual rollout:

  • Last old-schema (devicestatus has no loop key) record: 2026-10-26T02:13:46.951Z
  • First new-schema (devicestatus.loop present, null) record: 2026-10-26T02:13:48.576Z

treatments — new uploader (current): every bolus/carb/SMB entry now carries both fields, with timestamp explicitly null:

{
  "eventType": "Bolus",
  "insulin": 7.4,
  "created_at": "2026-10-26T19:57:32.786Z",
  "timestamp": null,
  "enteredBy": "Trio",
  ...
}

(plus ~70 other fields the Nightscout treatment schema defines, all null when unused).

Confirmed crash sites

1. DeviceStatus.swift — top-level pump/loop/openaps (initial crash)

if let lastPumpRecord = lastDeviceStatus?["pump"] as! [String: AnyObject]? { ... }
if let lastLoopRecord = lastDeviceStatus?["loop"] as! [String: AnyObject]? { ... }
if let lastLoopRecord = lastDeviceStatus?["openaps"] as! [String: AnyObject]? { ... }

"loop": null now being always present turns this into a guaranteed crash on every launch, on the main thread, inside webLoadNSDeviceStatus()'s device-status handling — matching the crash report's stack (dispatch_main_queue_drain → app frames → swift_dynamicCastFailure).

2. Treatment timestamp parsing (second crash, surfaced only after fix #1 was applied)

Repeated in SMB.swift, Carbs.swift, Bolus.swift, Notes.swift, and 3 call sites in StatsDataFetcher.swift:

if currentEntry["timestamp"] != nil {
    bolusDate = currentEntry["timestamp"] as! String
} else if currentEntry["created_at"] != nil {
    bolusDate = currentEntry["created_at"] as! String
} else {
    continue
}

With "timestamp": null present on virtually every treatment document under the new uploader, this crashes the first time any bolus/carb/SMB/note graph data is loaded or refreshed.

Fix

Both sites are fixed by replacing != nil + as! with a safe-cast fallback chain, e.g.:

guard let bolusDate = currentEntry["timestamp"] as? String ?? currentEntry["created_at"] as? String else {
    continue
}

and as? instead of as! for the DeviceStatus.swift dictionary casts. A PR with this fix will be linked here shortly.

Other latent (not-yet-crashing) sites with the same pattern

Found via a grep for as! across the Nightscout parsing code; not confirmed as currently reachable with my data, but fragile given how pervasive explicit-null encoding is under the new uploader style, and worth a follow-up pass:

  • SAge.swift (lines ~46, ~65)
  • CAge.swift (line ~42)
  • IAge.swift (line ~47)
  • DeviceStatusLoop.swift (lines ~68, ~111) — only reached for Loop-type device, not Trio
  • DeviceStatusOpenAPS.swift:223 — gated behind a key (recommendedTempBasal) Trio doesn't currently send

Suggested general guideline

Anywhere LoopFollow parses a Nightscout JSON document, dict["key"] != nil is not a safe guard for "field has a usable value" — it only tests for key presence, and an explicit JSON null (now common with full-schema uploaders like Nocturne) passes that check. The safe pattern is as? casting directly (which returns nil for both a missing key and an NSNull value), not a separate presence check followed by a force cast.

Activity

  1. bjorkert commented on Nov 1, 2026

    @bjorkert
    Member

    Fixed by #797, merged into dev.

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

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