Skip to content

ProviderError variants

Every ReleaseProvider method returns Result<_, ProviderError>. The enum is #[non_exhaustive] — match with a _ arm — and derives Clone so an error can be stashed in a retry policy or a rendered diagnostic without losing the underlying io::Error.

How an HTTP response becomes an error

Every REST backend routes non-2xx responses through one shared mapping, in this order:

Condition Variant
status is 2xx Ok(())
status is 429 Too Many Requests RateLimited
GitHub only: 403 and X-RateLimit-Remaining: 0 RateLimited
status is 401 Unauthorized Unauthorized
status is 404 Not Found NotFound
anything else Transport

The last row is the one that surprises people: a plain 403 Forbidden, a 500, and a 502 all arrive as Transport, not as Unauthorized or a dedicated server-error variant. A GitHub token with insufficient scopes produces a 403 without rate-limit headers, so it reads as a transport failure. A GitLab or Gitea token that is merely under-scoped often produces a 404 instead, because those hosts hide resources you cannot see.

NotFound

release or asset not found: {what}

Diagnostic code rtb::vcs::not_found.

Produced by a 404, and by two backend-specific cases:

  • GitLab latest_release when the newest release is an upcoming release — what is the literal string latest release.
  • Bitbucket latest_release on a repository with no tags — what is no tags available for latest release.
  • Direct release_by_tag when pinned_version is set and the requested tag does not equal it — what is the requested tag.

For a 404, what is the status line rather than the thing you asked for (404 Not Found). Do not parse it; keep your own record of what you requested.

A draft release requested without a token also lands here: the host returns a 404 rather than admitting the release exists.

Unauthorized

authentication failed for {host}

Diagnostic code rtb::vcs::unauthorized, with the help text "check the credential registered for this release source".

Produced only by a 401. The host field is the normalised host the provider was built with, not the full request URL.

RateLimited

rate limited by {host}; retry after {retry_after:?}

Diagnostic code rtb::vcs::rate_limited.

retry_after: Option<Duration> is filled in from the response headers:

  1. Retry-After, if it parses as an integer number of seconds. A Retry-After in HTTP-date form is not understood and is skipped.
  2. Otherwise X-RateLimit-Reset, read as Unix epoch seconds, and only if it is in the future. The duration is the difference from now.
  3. Otherwise None.

None means "the host did not tell us", not "retry immediately". Apply your own backoff.

Transport

network transport error: {0}

Diagnostic code rtb::vcs::transport. Two quite different situations produce it:

  • The request never completed — DNS failure, TLS failure, connection refused, or the configured timeout_seconds elapsing. The payload is reqwest's stringified error.
  • The response arrived with a status the mapping does not recognise. The payload is unexpected status {status} from {host}.

Check for that unexpected status prefix if you need to tell a server error apart from a network error.

MalformedResponse

response body could not be parsed: {0}

Diagnostic code rtb::vcs::malformed_response. The host answered with 2xx and a body that did not deserialise into the expected shape. Most often a proxy or captive portal returning an HTML error page with a 200.

The Direct backend adds two of its own:

direct version_url JSON missing `.version` string
direct version_url returned empty body

Unsupported

operation is not supported by this provider

Diagnostic code rtb::vcs::unsupported, with help text pointing at latest_release / release_by_tag.

Produced by exactly one call in the crate: list_releases on the Bitbucket backend. Bitbucket Cloud has no list-releases endpoint, and synthesising one would mean a downloads lookup per tag. There is no configuration that makes Bitbucket list_releases work.

The variant carries no fields, so a tool that supports several backends should treat it as "this capability is absent here" and degrade, not retry.

InvalidConfig

provider configuration is invalid: {0}

Diagnostic code rtb::vcs::invalid_config. Raised by a factory before any network call. The full set of messages and their triggers is in Release source configuration. The categories are:

  • A required parameter is empty or whitespace.
  • A host or version_url uses http:// where HTTPS is required.
  • A direct template lacks {version}.
  • A factory was handed a config for a different backend.
  • reqwest could not build a client — reqwest build failed: {e}. This is a TLS-stack problem, not a config typo.

An InvalidConfig never becomes valid by retrying.

Io

I/O error: {0}

Diagnostic code rtb::vcs::io. Wraps Arc<std::io::Error> — the Arc is what keeps the whole enum Clone without discarding the error kind. There is a From<std::io::Error> impl, so ? on an I/O error inside a custom backend produces this variant.

Cloning a ProviderError::Io shares the same Arc; both copies report the same ErrorKind.

Which variants can each backend produce?

Variant GitHub GitLab Gitea / Codeberg Bitbucket Direct
NotFound yes yes yes yes yes
Unauthorized yes yes yes yes yes
RateLimited yes (429 and 403) yes (429) yes (429) yes (429) yes (429)
Transport yes yes yes yes yes
MalformedResponse yes yes yes yes yes
Unsupported no no no list_releases only no
InvalidConfig factory factory factory factory factory
Io only from custom backends