Skip to content

Don't let a pyatv error during start up kill the child bridge - #1405

Merged
maxileith merged 1 commit into
maxileith:developfrom
seidnerj:fix-startup-unhandled-rejection
Jul 28, 2026
Merged

maxileith merged 1 commit into
maxileith:developfrom
seidnerj:fix-startup-unhandled-rejection

Conversation

@seidnerj

Copy link
Copy Markdown
Contributor

Problem

Any error raised while an accessory is starting up takes the whole child bridge down. From my log:

Error: Got pyatv Error: cannot convert float NaN to integer
    at parseState (.../@sebbo2002/node-pyatv/src/lib/tools.ts:239:15)
    at A.getState (.../@sebbo2002/node-pyatv/src/lib/device.ts:535:30)
    at AppleTVEnhancedAccessory.startUp (src/appleTVEnhancedAccessory.ts:1704:17)

[Apple TV Enhanced] Child bridge ended (code 1, signal null).
[Apple TV Enhanced] Child bridge will automatically restart in 20 seconds (restart attempt 2 of 4).

Homebridge only retries four times, so a device that is unreachable or misbehaving for a couple of
minutes leaves the accessory gone until Homebridge is restarted by hand.

Cause

validationLoop in the accessory constructor discards three promises with void:

//FIXME: catch errors / remove void
void this.credentialsValid().then((valid: boolean): void => {
    if (valid) {
        this.log.success('Credentials are still valid. Continuing ...');
        void this.startUp();
    } else {
        this.log.warn('Credentials are no longer valid. Need to repair ...');
        //FIXME: catch errors / remove void
        void pairingRequired().then(validationLoop.bind(this));
    }
});

All three talk to the device through pyatv, and startUp() in particular does a lot of it
(getPowerState, getDeviceState, getMediaType, createVolumeFan -> getState, listApps).
void throws the promise away along with its rejection, so nothing is left holding it; on Node 18+
an unhandled rejection terminates the process by default, which is the code 1 above. The two
FIXME: catch errors / remove void comments already mark this.

Change

Keep hold of the chain and attach a catch that logs the failure and retries the whole validation
loop after a delay, rather than letting the rejection escape. Both FIXMEs are addressed; the happy
path is unchanged.

Testing

npm run build passes (lint at --max-warnings=0 + tsc). Running on a Raspberry Pi with three Apple
TVs; bridge starts normally and the accessories behave as before. The retry path itself only triggers
on a pyatv failure, so it is exercised by the fault rather than by the happy path.

Note

The NaN error that led me here is a pyatv bug and I have a fix open for it separately
(postlund/pyatv#2897). This change is independent of that one: it is about any pyatv error during
start up being fatal to the child bridge rather than recoverable.

validationLoop discarded three promises with `void`: credentialsValid(),
startUp() and pairingRequired(). All three talk to the device through pyatv,
so one transient error became an unhandled rejection and took the child
bridge process down with it. Once Homebridge exhausts its four restart
attempts the accessory stays gone until Homebridge is restarted by hand.

Keep hold of the chain and attach a catch that logs the failure and retries
the whole validation loop after a delay, addressing the two FIXMEs that were
already sitting on those calls.
@maxileith
maxileith merged commit e638596 into maxileith:develop Jul 28, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants