Every exception the SDK raises for something that went wrong at runtime inherits Envless::Error, a StandardError, so one rescue Envless::Error catches all of them. A mistake in the call itself, such as an empty slug, a limit: that is not an Integer or binary data passed as plaintext, raises ArgumentError instead, before anything is sent.
| Exception | Raised when |
|---|---|
Envless::ApiError | The API answered and the answer was a failure. Carries status, code and the request id. Also raised before sending when a variable name is unusable. |
Envless::NetworkError | Nothing came back to classify: connection refused, DNS failure, a TLS error, or the timeout passing. |
Envless::DecryptionError | A value cannot be decrypted with the passphrase or key you passed, or is not an Envless envelope at all. |
Envless::WebhookVerificationError | A webhook delivery fails signature, timestamp or payload checks in verify_webhook_signature. |
require "envless/sdk"
begin
Envless.projects.create(name: "Billing", slug: "billing")
rescue Envless::ApiError => error
if error.needs_upgrade?
puts "Upgrade needed: #{error.message}"
elsif error.scope_missing?
puts "Key needs: #{error.resource}"
elsif error.conflict?
puts "Slug already taken"
else
puts "#{error.code} #{error.request_id}"
end
rescue Envless::NetworkError => error
puts "Could not reach the API: #{error.message}"
puts "Timed out" if error.timeout?
endNever branch on error.message. It is written for humans and can be reworded at any time. error.code is a stable contract.
An Envless::ApiError nobody rescues prints its status, its code and the request id next to the message, so a crashed Rake task or job log already carries what support needs.
Envless::ApiError
| Attribute | Type | Notes |
|---|---|---|
status | Integer or nil | The HTTP status. A variable name refused before sending carries 400, the status the API would answer with. |
code | String | The stable code, such as variable.ciphertext_invalid. Always present, falling back to server.error. |
message | String | The API's human readable message. |
resource | String or nil | What the failure relates to. On a scope failure this is the exact scope needed, such as variables:write. |
field | String or nil | The request field that caused it. |
request_id | String or nil | Read from the response's _req.reqId, or its Request-Id header. nil on a refusal made before anything was sent. This is what support needs. Log it. |
retry_after_seconds | Numeric or nil | From the Retry-After header, on a 429 or a transient 5xx. |
Predicates
| Method | True when |
|---|---|
auth? | code starts with auth. |
scope_missing? | code is exactly auth.scope_missing |
validation? | status is 400 |
needs_upgrade? | status is 402 |
not_found? | status is 404 |
conflict? | status is 409 |
rate_limited? | status is 429 |
They cover the branches most callers need. For anything finer, match on code, which reads well in a case:
require "envless/sdk"
begin
Envless.variables.get("api", "production", "STRIPE_SECRET_KEY")
rescue Envless::ApiError => error
case error.code
when "auth.scope_missing"
warn "Mint a key with #{error.resource}"
when "project.ambiguous_product"
warn "Pass product: to pick the project"
else
raise
end
endEnvless::NetworkError
| Attribute | Type | Notes |
|---|---|---|
message | String | What went wrong, without the token or the request body. |
timeout? | true or false | true when the timeout passed before the response finished. |
cause | Exception or nil | The underlying Net::HTTP, socket or OpenSSL error, as Ruby chains it. |
The codes worth handling
Full tables live on each endpoint page. These are the ones that come up when you drive the API from a script.
| Code | Status | What to do |
|---|---|---|
auth.api_key_not_found | 401 | The key is invalid, inactive or revoked. Stop, do not retry. |
auth.scope_missing | 403 | Mint a key with error.resource. |
auth.scope_ceiling_exceeded | 403 | You asked for scopes the minting key does not hold. |
auth.key_project_restricted | 403 | The key is fenced to other projects. |
auth.product_restricted | 403 | The key owner has no grant on a private product. |
request.body_invalid | 400 | Fix the payload. error.field names the offender. |
request.invalid_pagination | 400 | limit outside 1 to 100. |
variable.ciphertext_invalid | 400 | You sent plaintext. See Encryption. |
variable.baseline_drifted | 409 | Somebody edited since you read the baseline. Re-read and retry. |
project.ambiguous_product | 409 | Pass product:. |
plan.upgrade_required | 402 | A tier limit or a paid feature, such as an older version or a rollback on Free. An upgrade fixes it. Not retryable. |
plan.limit_reached | 409 | The 1,000-variables-per-environment fair-use cap. No plan raises it; contact support. Not retryable. |
Retrying safely
The client already retries transient failures on idempotent methods. What it deliberately does not do is retry a POST or a PATCH, because a create that timed out may have landed. If you want to retry a write, make it safe first.
For variables.apply the baseline makes this genuinely safe: a retry either applies cleanly or fails with variable.baseline_drifted and writes nothing, so the loop below re-reads the environment and tries again.
require "envless/sdk"
3.times do |attempt|
current = Envless.variables.list_all("api", "production")
baseline = current.map do |variable|
{
uniqueId: variable[:id],
name: variable[:name],
updatedAt: variable[:updatedAt] || variable[:createdAt]
}
end
deletes = current.filter_map do |variable|
next unless variable[:name] == "LEGACY_API_URL"
{uniqueId: variable[:id]}
end
Envless.variables.apply("api", "production",
baseline:, deletes:)
break
rescue Envless::ApiError => error
drifted = error.code == "variable.baseline_drifted"
raise unless drifted && attempt < 2
endLogging
Log the code and the request id, never the payload. Variable values are ciphertext, but names, member emails and workspace slugs are not.
require "logger"
require "envless/sdk"
logger = Logger.new($stderr)
begin
Envless.variables.delete("api", "production", "LEGACY_API_URL")
rescue Envless::ApiError => error
logger.error(
"envless request failed code=#{error.code} " \
"status=#{error.status} resource=#{error.resource} " \
"field=#{error.field} request_id=#{error.request_id}"
)
raise
end