Skip to main content

AuthenticatedClient

Struct AuthenticatedClient 

Source
pub struct AuthenticatedClient { /* private fields */ }
Expand description

The one client every api.github.com request in this crate goes through.

It exists to make four things structural rather than remembered:

  1. X-GitHub-Api-Version, Accept, User-Agent, and Authorization are set on every request because they are set here.
  2. The 401 / 403 taxonomy of 03-control-flows.md flow 4.3 is implemented once. c3 and f1 both branch on the distinction, and two implementations of it would eventually disagree.
  3. A 401 storm produces one credential re-validation, not one per caller — see AuthenticatedClient::revalidate.
  4. A lockout stops traffic. Once GitHub answers 403 after 401s, this client issues no further HTTP at all until the back-off elapses.

Implementations§

Source§

impl AuthenticatedClient

Source

pub fn new( endpoints: Endpoints, credential: UserAccessToken, clock: Arc<dyn Clock>, ) -> Result<Self, GithubError>

§Errors

The HTTP client failing to build — a TLS backend that will not initialise, in practice.

Source

pub fn with_http_client( http: Client, endpoints: Endpoints, credential: UserAccessToken, clock: Arc<dyn Clock>, ) -> Self

Source

pub fn endpoints(&self) -> &Endpoints

Source

pub fn revalidations_performed(&self) -> u64

How many credential re-validations this client has performed.

Public because it is the observable the single-flight requirement is stated in terms of: “concurrent callers hitting 401 together produce one attempt, not N”.

Source

pub fn is_locked_out(&self) -> bool

true while a lockout back-off is still running, during which this client issues no HTTP at all.

§Panics

If a previous holder panicked while the lockout lock was held.

Source

pub fn lockout_remaining(&self) -> Option<Duration>

How much of the lockout back-off is left, or None when not locked out.

§Panics

If a previous holder panicked while the lockout lock was held.

Source

pub fn clear_lockout(&self)

Clear a lockout early. f1 does not need this — the back-off expires on its own against the clock — but a successful interactive auth login legitimately invalidates the whole lockout premise.

§Panics

If a previous holder panicked while the lockout lock was held.

Source

pub async fn send( &self, request: &ApiRequest, ) -> Result<ApiResponse, GithubError>

Send one request, applying the authentication taxonomy.

On 401 this performs a single-flight credential re-validation and then one retry — never more, and never a token renewal, because there is nothing to renew (see AuthenticatedClient::revalidate).

§Errors

Every variant of GithubError.

Source

pub async fn get_json<T: DeserializeOwned>( &self, path: &str, ) -> Result<T, GithubError>

Deserialize a GET in one step.

§Errors

Every variant of GithubError.

Source

pub fn with_renewal(self, renewal: Arc<dyn CredentialRenewal>) -> Self

Attach a way for this client’s credential to replace itself.

Source

pub fn with_credential_source(self, source: Arc<dyn CredentialSource>) -> Self

Attach the store this client’s credential came from, so a 401 it cannot renew its way out of can still notice a sign-in that already happened.

Source

pub async fn post_json<B: Serialize, T: DeserializeOwned>( &self, path: &str, body: &B, ) -> Result<T, GithubError>

Serialize, POST, and deserialize in one step.

§Errors

Every variant of GithubError.

Source

pub async fn revalidate(&self) -> Result<Revalidation, GithubError>

Re-validate the credential once, no matter how many callers ask at once.

§This is not a token renewal — renewal is Self::renew_once

03-control-flows.md flow 4.3 says a 401 “triggers one refresh under a single-flight mutex, then one retry”, and that is implemented literally: one attempt shared by every concurrent caller, then one retry each.

This method used to claim the word “refresh” could not be meant, because renewing needs a client secret and the App issues no renewal token. Both halves were false; see this module’s header. Renewal exists, it runs first in Self::revalidate_and_retry_once, and what is left here is the path for a credential that has no refresh half to spend — one stored before 0.1.11, or issued while the App had expiration switched off.

So the single thing that happens under the mutex is a re-validation of the credential already held: one GET /user/installations with the same token, asking GitHub whether it still accepts it. A Revalidation::Rejected answer is terminal GithubError::AuthenticationFailed requiring an interactive auth login; Revalidation::Valid and Revalidation::Unavailable both spend the one retry.

§Position, and what it no longer decides on its own

This method is the caller’s own probe: it is a first attempt by construction, whatever happened on some other request minutes ago, so it passes [Attempt::First] down.

That used to settle the matter. This heading read “why this entry point may not latch a lockout”, and the text said the probe it drives cannot latch — an accurate description of the position rule as it then stood, and a false statement about the product. A lockout that outlives one back-off continues on a first attempt by construction, because this client’s retry never happened: the request never reached the wire. Refusing to latch there stopped the back-off entirely and hammered a credential GitHub had asked to be left alone.

So position alone no longer decides. AuthenticatedClient::is_lockout_403 reads GitHub’s own evidence in the first position instead: a first attempt latches when, and only when, the response carries retry-after and no parseable GitHub message body. A permissions refusal names what is not accessible, so it still does not latch, which is what keeps GithubError::Forbidden reachable from here.

revalidate_and_retry_once uses the private AuthenticatedClient::revalidate_after_unauthorized instead, which is in the retry position, where any 403 that is not a rate limit is the lockout regardless of what the body says.

§This call can latch a lockout, and then it says so

Because a first attempt can latch, this call can leave the whole client backed off for up to MAX_LOCKOUT_BACKOFF. It reports that as GithubError::AuthenticationLockout rather than answering Ok(Revalidation::Unavailable) and leaving the caller to discover it through a separate AuthenticatedClient::is_locked_out call. An auth status that printed “could not determine” while the client it had just silenced sat mute for fifteen minutes would be reporting the wrong event, and reporting it as the milder one.

§Errors

GithubError::AuthenticationLockout if this client is already backing off when the call arrives, or if this call’s own probe latches one.

A credential GitHub has rejected outright is not an error here: that comes back as Ok(Revalidation::Rejected), and what to do about it — prompt for auth login — is the caller’s decision, not this method’s.

§Panics

If a previous holder panicked while the re-validation result lock was held.

Source§

impl AuthenticatedClient

Source

pub async fn discover_installations( &self, app: &AppRegistration, ) -> Result<InstallationDiscovery, GithubError>

Which repositories and organizations the stored credential can actually reach.

Two calls, both paginated: GET /user/installations, then GET /user/installations/{id}/repositories per installation. The shapes are the ones the D18 spike observed live (docs/spikes/d18-org-jit-verification.md, “The permission that authorized it”).

An installation is reported even when it is broader than the user expected — Installation::is_over_broad — because 07-security.md requires that an over-broad installation be visible rather than assumed. Nothing here narrows or hides one.

§Errors

Every variant of GithubError. A 401 here goes through the same single-flight re-validation as any other request.

Trait Implementations§

Source§

impl Debug for AuthenticatedClient

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more