fix(client): use form-urlencoded for OAuth token endpoint#24
Merged
omarshahine merged 5 commits intosteipete:mainfrom Apr 16, 2026
Merged
fix(client): use form-urlencoded for OAuth token endpoint#24omarshahine merged 5 commits intosteipete:mainfrom
omarshahine merged 5 commits intosteipete:mainfrom
Conversation
The Eight Sleep auth server (auth-api.8slp.net/v1/tokens) expects standard OAuth2 form-urlencoded requests, not JSON. The previous implementation sent JSON with hardcoded "sleep-client" credentials, which caused a 400 from Joi validation. The fallback to legacy /login then tripped the rate limiter, resulting in a permanent 429 loop. Changes: - Send application/x-www-form-urlencoded instead of application/json - Use c.ClientID and c.ClientSecret (the real app creds extracted from the Android APK) instead of hardcoded "sleep-client"/"" - Make authURL a var so tests can point it at a local server - Add tests for form encoding, credential passthrough, and legacy login fallback Fixes steipete#7, fixes steipete#8, fixes steipete#12 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The do() method sends Accept-Encoding: gzip but never decompresses
the response body, causing json.Decode to fail with:
invalid character '\x1f' looking for beginning of value
(0x1f is the gzip magic byte.)
Check Content-Encoding: gzip on responses and wrap the body in a
gzip.Reader before decoding. Added test with a mock gzip response.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4 tasks
Collaborator
Author
…dless envs - Replace infinite 429/401 retry loops with bounded retries (max 3) and exponential backoff to prevent permanent rate-limit storms - Remove explicit Accept-Encoding: gzip header; let Go's http.Transport handle compression transparently (simpler, no manual gzip.NewReader) - Detect headless environments (SSH, no TTY, EIGHTCTL_KEYRING_FILE=1) and fall back to file-based keyring to avoid macOS Keychain hang Credit to @davidfencik (steipete#16) for the retry and keyring patterns, and @petersentaylor (steipete#27) for the simpler gzip approach. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This was referenced Apr 15, 2026
The legacy /login endpoint no longer works reliably upstream, and the silent fallback was masking real OAuth errors. Remove it so OAuth failures surface directly. Also revert the keyring-backend tweaks from 9f49cf0 — leave tokencache behavior identical to main. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
6ce25bf to
2bda5c8
Compare
`disable-all: true` and `disable: [errcheck, unused]` can't be combined — golangci-lint errors with "can't combine options --disable-all and --disable". Remove the redundant `disable` list and stale `revive` settings since only `govet` is enabled. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2bda5c8 to
b573230
Compare
Collaborator
Author
Code Review+164 / -79 lines across 4 files, 5 commits OverviewFixes the three interconnected bugs that made eightctl non-functional: wrong OAuth encoding, explicit gzip header disabling Go's transparent decompression, and unbounded retry loops. Also removes the dead legacy Correctness
Test Coverage
Notes
Reviewed and tested locally. CI passing. |
Collaborator
Author
2 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Three bugs in the API client that make
eightctlunreliable or non-functional:1. Wrong OAuth content type and credentials in token endpoint
authTokenEndpoint()sendsapplication/jsonbut the Eight Sleep auth server (auth-api.8slp.net/v1/tokens) expects standard OAuth2application/x-www-form-urlencoded. It also hardcodesclient_id: "sleep-client"with an emptyclient_secretinstead of using the actual app credentials.The JSON request always returns 400 (Joi validation), causing every authentication to fall through to
authLegacyLogin(). After a few attempts the rate limiter kicks in.Note: PR #15 correctly identified the credential issue but kept JSON encoding, which is why the token endpoint still fails even with that fix applied.
2. Gzip responses not decompressed
do()setsAccept-Encoding: gzipexplicitly, which disables Go's automatic transparent decompression. The response body arrives as raw gzip bytes, causing:3. Infinite retry loops on 429 and 401
Both 429 (rate limited) and 401 (unauthorized) responses trigger unbounded recursive retries with no backoff, leading to permanent loops when auth is broken.
Fix
OAuth auth:
application/x-www-form-urlencodedcontent type (standard OAuth2 token request format)c.ClientIDandc.ClientSecretinstead of hardcoded"sleep-client"/""authURLa package-levelvar(wasconst) so tests can redirect it to a local serverDrop legacy /login fallback:
/loginendpoint no longer works reliably upstreamAuthenticate()now calls the token endpoint directlyGzip decompression:
Accept-Encoding: gzipheader fromdo()http.Transporthandles gzip transparently when the header is not set manuallyBounded retries:
Tests added:
Tested against a live Pod 2 Pro.
Fixes #5, fixes #7, fixes #8, fixes #12, fixes #14, fixes #29
Note: Issue #10 (keyring hang in headless environments) is not addressed in this PR. A separate PR with the headless keyring fallback (credit to @davidfencik) can follow.