Skip to content

Fix /pairings handler rejecting ListPairings with "tlv8: EOF"#67

Open
hughobrien wants to merge 71 commits into
brutella:masterfrom
hughobrien:fix-pipelined-request-loss
Open

Fix /pairings handler rejecting ListPairings with "tlv8: EOF"#67
hughobrien wants to merge 71 commits into
brutella:masterfrom
hughobrien:fix-pipelined-request-loss

Conversation

@hughobrien

@hughobrien hughobrien commented Jun 19, 2026

Copy link
Copy Markdown

Summary

The /pairings handler decodes requests into a struct that marks Identifier (tag 1) as required. A ListPairings request contains only Method (tag 0) and State (tag 6); it has no Identifier. The tag-keyed reader returns io.EOF for the missing required field, so tlv8.UnmarshalReader fails and the handler responds HTTP 400, logging tlv8: EOF.

iOS issues ListPairings to enumerate an accessory's paired controllers. With the request always failing, the controller set never reconciles; ListPairings, RemovePairing, and pair-verify are retried indefinitely.

Change

Mark Identifier optional. 340cbe4 (#21) previously made PublicKey and Permission optional but left Identifier required, so ListPairings still failed. Fixes #21 and #44.

Captured requests

pairings() was instrumented to log ContentLength, the read body length and error, and the raw bytes immediately before tlv8.UnmarshalReader. Observed during pairing with a real controller:

method=POST CL=6  bodyLen=6  readErr=<nil> body=000105060101
tlv8: EOF
method=POST CL=81 bodyLen=81 readErr=<nil> body=000103060101012437463141...0b0101   (AddPairing, accepted)
method=POST CL=44 bodyLen=44 readErr=<nil> body=000104060101012443433838...          (RemovePairing, accepted)

The failing body is fully read (bodyLen=6, readErr=<nil>); it is not empty or truncated. Decoding 000105060101:

00 01 05   Method (tag 0), len 1 = 0x05  (ListPairings)
06 01 01   State  (tag 6), len 1 = 0x01
           no Identifier (tag 1)

Test

TestPairingsHandlerRequests replays the captured bodies against the handler.

master:

=== RUN   TestPairingsHandlerRequests
INFO pairings.go:41: tlv8: EOF
    pairings_test.go:56: ListPairings: handler returned HTTP 400, want 200
--- FAIL: TestPairingsHandlerRequests

this PR:

=== RUN   TestPairingsHandlerRequests
--- PASS: TestPairingsHandlerRequests (0.00s)

brutella and others added 30 commits March 7, 2022 14:39
Changes to a single style for all accessories setup, including using AddS() rather than a manual append().
Also cleans up spacing and formatting.
brutella and others added 23 commits August 14, 2023 15:42
Some control points require that the response be returned after a write,
as opposed to write-then-read. If the SetValueRequestFunc returns a
value and has the WriteResponse permission, then the response is relayed
back to the client on return. Status code must be explicitly returned in
this case, via a MultiStatus return.
Implement WriteResponse characteristics
HomeKit expects an application/pairing+tlv8 content type for responses
from /pairings, and fails to add the accessory if it gets something
different.

You can reproduce the error by attempting to add an accessory as a
user who is an admin but not the owner of the home. This will result
in a flow that will attempt to use the AddPairing method. On all my
devices this fails unless the content type is application/pairing+tlv8.
use correct content-type for /pairings
feat: add motion and contact sensor accessories
fix: wrong bytes written for conn when encrypting
You can now specify an id for a service and characteristic. Since this ids have to be unique within an accessory, a check was added to make sure that the ids are actually unique.
@hughobrien

Copy link
Copy Markdown
Author

Hello, while the above is Claude I am an actual human (promise!) and have been having a lot of fun with your library in some home automation projects (A, B). I have an Apple TV and 2x Homepods so I may be encountering this more than most. Let me know if I can provide any additional context.

The /pairings handler decoded the request into a struct that marked
Identifier (tag 1) as required. A ListPairings request carries only
Method (tag 0) and State (tag 6) — no Identifier — so the tag-keyed
reader returned io.EOF for the missing required field and the handler
rejected the request (HTTP 400, "tlv8: EOF").

iOS uses ListPairings to reconcile a home's controllers (resident
HomePods/Apple TVs, additional devices). Because the request always
failed, iOS could never converge: it retried endlessly, re-attempting
RemovePairing for controllers it wanted to drop and leaving others
looping on pair-verify ("not paired with <id> yet").

Mark Identifier optional, completing the partial fix in 340cbe4 (brutella#21)
which made PublicKey and Permission optional but left Identifier
required. Resolves the unmarshal failure reported in brutella#21 and brutella#44.

Added a handler-level regression test using request bodies captured
from a real iOS controller.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hughobrien hughobrien force-pushed the fix-pipelined-request-loss branch from bd1f7e7 to 78932fb Compare June 19, 2026 20:27
@hughobrien hughobrien changed the title Fix silent loss of pipelined requests on encrypted connections Fix /pairings handler rejecting ListPairings with "tlv8: EOF" Jun 19, 2026
@hughobrien hughobrien force-pushed the fix-pipelined-request-loss branch from 78932fb to ae235b3 Compare June 29, 2026 03:10
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.

pairings.go:41: tlv8: EOF

8 participants