Skip to content

Report a consumer the API no longer accepts - #108

Merged
adriankumpf merged 1 commit into
masterfrom
report-a-rejected-consumer
Aug 20, 2026
Merged

Report a consumer the API no longer accepts#108
adriankumpf merged 1 commit into
masterfrom
report-a-rejected-consumer

Conversation

@adriankumpf

Copy link
Copy Markdown
Owner

A long-running client renews its access token with reauthorize/3 when a data endpoint answers 401. That works until the consumer expires as well, which the API tends to do at the same nightly maintenance. From then on the renewal fails and no retry gets anywhere, because only a new consumer can help and registering one means calling login/3.

There was no way to tell that apart. /oauth1/request_token answers a consumer it no longer accepts with a 400 and an empty body, so the failure arrived as a bare {:http_error, 400}, indistinguishable from a malformed request. My smartmeter deployment hit exactly this: the token expired at 03:52, then four hours of 400s at four-minute intervals until a restart signed in from scratch.

The fix

get_request_token/2 reports it as reason: :consumer_rejected, which is the signal to call login/3:

case Discovergy.Client.reauthorize(client, email, password) do
  {:error, %Discovergy.Error{reason: :consumer_rejected}} ->
    Discovergy.Client.login(client, email, password)

  result ->
    result
end

Only an empty body is mapped, so an error the API bothered to write keeps its message. Both 400 and 401 are covered, because request_token is posted with no parameters of its own: the consumer key and its signature are the only thing it can refuse.

Exception.message/1 also gained clauses for :consumer_rejected and :not_logged_in, which rendered as inspected atoms (":consumer_rejected") in log lines until now.

Verified against the API

Confirmed on the demo account, which is where the numbers below come from:

request result
request_token, valid key and secret 200
request_token, valid key, wrong secret 400, empty body
request_token, unknown key 400, empty body
meters, unknown consumer 401, empty body

So the data endpoints use 401 for credentials they no longer accept, and this one uses 400. End to end, with a deliberately dead consumer, the documented recovery now works:

consumer rejected -> registering a new one
last reading: %Discovergy.Measurement{time: ~U[2026-08-20 08:17:00.000Z], values: %{"energy" => 26748013000000, "power" => -162000}}

The ordinary path is unaffected: login/3 followed by reauthorize/3 reuses the consumer and gets a fresh token, all three steps 200.

Docs

guides/api-quirks.md gains a section on consumers expiring and the 400 that reports it, and the advice about persisting a consumer now says it can be rejected later. The reauthorize/3 docs carry the fallback snippet.

One correction in there: the snippet passes the existing client to login/3 rather than a fresh Client.new(). Since d7a1c7a login/3 discards the previous session's credentials itself, and reusing the client keeps its :base_url and :http_client, which Client.new() would silently reset to the defaults.

42 tests pass.

A long-running client renews its access token with reauthorize/3 when a
data endpoint answers 401. That works until the consumer expires as
well, which the API tends to do at the same nightly maintenance: from
then on the renewal fails and no retry gets anywhere, because only a new
consumer can, and registering one means calling login/3.

There was no way to tell that apart. /oauth1/request_token answers a
consumer it no longer accepts with a 400 and an empty body, so the
failure arrived as a bare {:http_error, 400}, indistinguishable from a
malformed request. Observed in production: an expired token at 03:52
followed by four hours of 400s, until a restart signed in from scratch.

The step now reports it as reason: :consumer_rejected, which is the
signal to call login/3. Only an empty body is mapped, so an error the
API bothered to write keeps its message, and both 400 and 401 are
covered because request_token takes no parameters of its own: the
consumer key and its signature are the only thing it can refuse.

Exception.message/1 gained clauses for :consumer_rejected and
:not_logged_in, which rendered as inspected atoms until now.
@adriankumpf
adriankumpf merged commit 7c322c0 into master Aug 20, 2026
4 checks passed
@adriankumpf
adriankumpf deleted the report-a-rejected-consumer branch August 20, 2026 08:24
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.

1 participant