Skip to content

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

Open
hughobrien wants to merge 1 commit into
brutella:masterfrom
hughobrien:fix-pipelined-request-loss
Open

Fix /pairings handler rejecting ListPairings with "tlv8: EOF"#67
hughobrien wants to merge 1 commit 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)

hughobrien added a commit to hughobrien/keylight-hap that referenced this pull request Jun 19, 2026
The bundled hap (v0.0.35) silently drops pipelined requests on encrypted
connections: conn.Read recreates and discards bufio's read-ahead each
Decrypt, losing the 2nd+ back-to-back request. During pairing this drops
the admin's AddPairing calls for home hubs (HomePods/Apple TV), so those
controllers get an empty body (pairings.go: tlv8: EOF) and never
register — they loop forever on "not paired with <id> yet".

Replace with hughobrien/hap@bd1f7e7, which persists the bufio.Reader
across Read/Decrypt. Submitted upstream as brutella/hap#67. Drop this
replace once that merges and is tagged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@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
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

1 participant