Repository navigation
clicksend 1.1.0
clicksend 1.1.0 adds failure semantics, observability and testing support for production messaging. It is mostly additive; read "Changed" below before upgrading.
gem install clicksend -v 1.1.0Highlights
- Explicit ambiguity:
rescue Clicksend::AmbiguousRequestErrorcatches every failure where ClickSend may or may not have accepted a send. The error keeps its class, so existingrescueclauses still work. - A retry rule no configuration can override. A send that may already have been processed is never repeated automatically.
Clicksend::RetryPolicyonly tunes timing and budget. - Request context on errors and responses, plus
retryable?andambiguous?. sms.history: the closest documented way to look for an ambiguous send. A missing row is not proof that nothing was sent.Clicksend::Testing::FakeAPI: an in-memory ClickSend for tests, with failure injection that asks whether ClickSend processed the request.- Instrumentation for
ActiveSupport::Notifications, without credentials, query strings or bodies. - Experimental: webhook parsing and rate-limit metadata, which rest on behaviour ClickSend doesn't document.
This reduces the risk of duplicate SMS. It is not a guarantee against every possible duplicate; see the README's "Background jobs" section.
Verified: the published gem is byte-identical to a rebuild of v1.1.0 (SHA-256 12948d70ff172c439f0c15e60d09af44f89edd28c2718c2d4925b7d378472a18). It was published with RubyGems Trusted Publishing.
Failure semantics, observability and testing support for production messaging. Mostly additive;
read "Changed" before upgrading. Two additions are experimental and may change in a minor
release: Clicksend::Webhook and Clicksend::RateLimit, because both rest on behaviour ClickSend
doesn't document.
Retry safety is unchanged in principle and stricter in practice. ClickSend has no idempotency key,
so a send that may already have been processed is never repeated automatically, and such failures
are now marked as ambiguous. This reduces the risk of duplicate SMS; it is not a guarantee against
every possible duplicate (for example, a job runner re-running a job after a crash).
Added
-
Explicit ambiguity. When a request that is not safe to repeat (such as an SMS send) fails
in a way that ClickSend may still have processed, the error is extended with
Clicksend::AmbiguousRequestError. That covers:- a timeout or reset after the request may have been written;
- a 5xx;
- an error reported inside a 2xx body;
- a 2xx answer the gem can't read, including a send result without a per-message status.
The error keeps its class, so existing
rescueclauses still work.Error#ambiguous?is the
predicate. (Adupof the error loses the mark;cloneand re-raising keep it.) -
Request context on errors and responses.
Error#requestandResponse#requestreturn a
Clicksend::RequestInfowithhttp_method,path(no query string or fragment),operation
(e.g."sms.deliver"),idempotentandattempts.Response#requestis not aData
member, soResponseequality,to_hand pattern matching are unchanged from 1.0. -
Error#retryable?: whether repeating the same request later is both safe and might succeed. -
Retry configuration.
Clicksend::RetryPolicyis public:
Client.new(retry_policy: RetryPolicy.new(max_retries:, base_delay:, max_delay:, max_retry_after:)).
The rule deciding which failures may be retried lives in the connection and cannot be
changed by any policy, and the connection enforces the policy's ownmax_retries. -
Rate limits (experimental).
Response#rate_limitandAPIError#rate_limitreturn a
Clicksend::RateLimit(limit,remaining,reset_in) from the rate-limit headers observed
live onGET /v3/account. They are nil when ClickSend sends none. -
Instrumentation.
Client.new(instrumenter:)acceptsActiveSupport::Notificationsor any
object with the sameinstrumentsignature that yields once. It publishesrequest.clicksend
andretry.clicksend. Payloads never include credentials, query strings or bodies, and the
wrapped methods' paths contain no phone numbers or message text.Client#requestand
#paginatetake an optionaloperation:label. -
Message history.
sms.history(date_from:, date_to:, to:/from:/status:/message_id:, order:)
returns a page ofClicksend::SMS::HistoryRecord, whosedelivered?,failed?andpending?
follow ClickSend's "SMS error codes" article and are all false when a row can't be classified
(e.g. "Completed" with no gateway code, as observed live). ClickSend documents no way to look up
a send by your own reference; history, filtered by recipient and matched oncustom_string, is
the closest. The README explains why a missing row is not proof that nothing was sent. -
Webhooks (experimental).
Clicksend::Webhook.parse_receipt,.parse_inboundand.parse
turn pushed receipts and replies intoSMS::ReceiptandSMS::InboundMessage. ClickSend
documents no way to authenticate pushes, so there is deliberately no verification method; the
README explains how to secure the endpoint. No real push has been captured yet. -
Testing.
require "clicksend/testing"addsClicksend::Testing::FakeAPI, an in-memory
ClickSend you plug in as the transport, covering sends, receipts, replies and the account. It
records sent messages and can inject failures, including ambiguous ones with an explicit
processed:flag. It deliberately doesn't serve history; stub it. Mistakes in stubs surface
asClicksend::Testing::StubError(not aStandardError), never as a simulated ClickSend
failure.
Changed
Behaviour an existing 1.0 application may notice:
- Mark-read without a cutoff is no longer retried.
sms.mark_receipts_readand
sms.mark_inbound_readwith nobefore:mark everything read at the moment ClickSend
processes the call. Retrying after an unknown outcome could hide items that arrived in between.
Withbefore:they are still retried. - Error messages end with the request they came from, e.g.
HTTP 500 (POST /v3/sms/send).
Code that matched the exact message text needs updating.MessageRejectednow carries
#requesttoo. - Unreadable 2xx answers to sends are ambiguous. Before, they were plain
MalformedResponseErrors. That coversdeliveranddeliver_batch, including a message
without a status, which 1.0 reported as a rejection (MessageRejectedwith a nil status, or
inBatch#rejected). - Custom transports:
- An exception that is not a
Clicksend::Errorbecomes aConnectionErrorthat may have been
sent. It is ambiguous for a send, retried for idempotent requests, and keeps the original as
#cause;rescue MyTransportErroraround client calls no longer matches. - Any other
Clicksend::Errorraised by a transport is treated as an unknown outcome. - A response without a valid HTTP status, or an unexpected 1xx/3xx, is ambiguous for a send
instead of being a rejection. - Exceptions raised by transports are copied before context is added.
- An exception that is not a
- Loggers and instrumenters cannot change a result. A logger or instrumenter that raises
after a request completes is logged and ignored. An instrumenter that never runs the block
raisesConfigurationError, and one that runs it twice can't send twice. - Configuration:
Client.new(max_retries:)andretry_policy:are mutually exclusive.Client#withreplaces
one with the other, andmax_retries: nilnow means the default.RetryPolicy.newvalidates its arguments (ConfigurationError) and returns a frozen policy.Client#request(idempotent:)honours onlytrue; other truthy values, such as1or
"false", no longer make a request retryable.
Compatibility
- Ruby 3.3 or newer, as before; tested on 3.3, 3.4 and 4.0, with a Ruby head canary in CI.
- Faraday 2 remains the only runtime dependency. ActiveSupport is used only in this gem's own
tests;instrumenter:duck-types it. - No public method or class from 1.0 was removed.
require "clicksend/testing"is opt-in and
not loaded byrequire "clicksend".
Verification
- Unit, integration (local real-socket servers) and contract specs cover the new behaviour.
Contract specs check the wrapped operations, the fields the models read, and the documented
page-size range against ClickSend's published OpenAPI files. - Live checks on 2026-10-06 were read-only:
sms.historyfiltered to ClickSend's test number,
and the rate-limit headers onGET /v3/account. No SMS was sent. - Not verified live: a webhook push (none has been captured), and a delivery receipt. The
test number produced none, as ClickSend's legacy docs say it won't. Both are parsed according
to ClickSend's published schemas and archived documentation.
Documentation
- README: positioning, unknown send outcomes and reconciliation, webhooks, history, background
jobs (including at-least-once job runners), instrumentation, and rate limits. Behaviour that
ClickSend doesn't document is labelled as inferred or observed. - API notes: a review of all 34 of ClickSend's OpenAPI sections plus the archived push
documentation; read-only live checks of history and rate-limit headers on 2026-10-06. design/1.1-audit-and-roadmap.md: the audit, the decisions taken, the features rejected, and
the reviews.