Breaking changes to the SDKs' public surface, by release, with what to change. Wire behavior
is not versioned here; the conformance fixtures under conformance/tests/ hold that.
HEY now answers GET /workflows/{workflowId}/stages/{stageId}.json, so GetWorkflowStage is
a JSON operation and the SDKs no longer read the stage's web page. The model's
WorkflowStageDetail is the stage, and each of its topics a StagedTopic: the topic as
GetTopic answers it, with staging_id, emails_count and notes_count.
The hand-written stage read keeps its signature and its answer everywhere:
Workflows::stage (Rust), client.workflows.stage (Kotlin, Swift) and Go's
WorkflowsService.GetStage still answer a WorkflowStageView of WorkflowStageTopics,
entry_count still the thread's email count. What changes:
- The generated
get_stage/getStage/getWorkflowStageanswers the stage rather than the page:WorkflowStageDetailin Rust (GetWorkflowStageResponseContent), Kotlin and Swift, and the stage object asdatain TypeScript, where it was astring.GetWorkflowStageOutputPayloadis gone. WorkflowStageView::parse(html, stage_id)(Rust),WorkflowStageView.parse(html, stageId)(Kotlin) andWorkflowStageView.parse(_:stageId:)(Swift) are gone, with the HTML parsers behind them. Convert whatget_stageanswers instead:
// before
let view = WorkflowStageView::parse(&page, stage_id)?;
// after
let view = WorkflowStageView::from(client.workflows().get_stage(workflow_id, stage_id).await?);Kotlin's is WorkflowStageView.from(stage), Swift's WorkflowStageView(stage).
- A stage HEY does not have is its 404,
not_foundas before (it was also what a page without the stage read as). - The Rust crate no longer depends on
scraperorego-tree, nor Go's module directly ongolang.org/x/net.
HEY's OAuth token endpoint is now a generated operation (CreateToken, RevokeToken) that goes
out unsigned. A client made only to call it holds no credentials and says so with
NoCredentials. A client given nothing at all is a forgotten token, so it is now refused when
it is built rather than failing at its first send:
- Go.
hey.NewClientpanics when given neither a token provider nor an auth strategy. Such a client already crashed on a nil pointer at its first send. Build a credential-less client withhey.WithAuthStrategy(hey.NoCredentials{}). - TypeScript.
new HeyClient(...)throws ausageHeyErrorwhentokenis neither a string nor aTokenProvider. Before, it failed with aTypeErrorat the first send. Passtoken: NoCredentials.
On a NoCredentials client, every signed operation fails auth_required before anything is
sent.
A Route written literally for Client::operation adds both fields:
// before
Route { id, service, method, /* … */ lenient_success: false, pagination, retry }
// after
Route { id, service, method, /* … */ lenient_success: false, unsigned: false, no_redirect: false, pagination, retry }false for both keeps the route as it was: signed, and following redirects as before.
A 304 need not repeat the Link and X-Total-Count of the 200 it confirms, so a cache
entry now holds the headers it was answered with, and a 304 is answered under them, updated
by whatever the 304 carried (RFC 9111 §4.3.4). CachedResponse gains a headers: HeaderMap
field. A ResponseCache of your own that builds one literally adds the field, and keeps the
headers it is handed in set to give them back from get:
// before
CachedResponse { etag, body }
// after
CachedResponse { etag, body, headers }An entry whose headers a cache cannot give back can use HeaderMap::new(); a 304 is then
answered under its own headers alone, as before. ResponseCache also gains revalidate,
which a 304 calls to keep its validator and headers; its default reads the entry and sets it
again, so a cache of your own needs no change for it. Override it to skip writing the body
again, or to make the check and update a single step (the default can put back an older but
still consistent entry over one stored meanwhile, which the next read refetches). FileCache writes them to
responses/<key>.headers, the file the Go SDK reads, and takes an entry written without one
for a miss, so each page is fetched in full once after the upgrade.
The calls that deliver a message used to answer nothing; they now answer a SentMessage,
HEY's answer for the delivery: the entry that went out (id), the thread it is on
(topic_id), its subject, and whether Undo Send is holding it back (delayed, with
notice, undo_action and undo_timeout while it is). A reply that breaks out into a
thread of its own on a Domains account names the new thread, not the one replied to.
| SDK | calls | was | now |
|---|---|---|---|
| Go | Messages().Create, Messages().Send, Messages().SendDraft, Entries().CreateReply |
error |
(*generated.SentMessage, error) |
| Rust | messages().send, messages().send_draft, entries().reply |
Result<(), Error> |
Result<SentMessage, Error> |
| Kotlin | messages.send, messages.sendDraft, entries.reply |
Unit |
SentMessage |
| Swift | messages.send, messages.sendDraft, entries.reply |
Void |
SentMessage (@discardableResult) |
In Go, a caller that only checked the error takes the answer too:
// before
if err := client.Messages().Create(ctx, subject, body, to, nil, nil); err != nil {
// after
sent, err := client.Messages().Create(ctx, subject, body, to, nil, nil)
if err != nil {Kotlin and Swift callers that ignore the answer need no change; Rust callers that ? the
call need none either.
Every member is optional, because a HEY that predates the ids answers {} — or only the
undo members while the delivery is delayed. Check for a zero or absent id and topic_id
before using one. The conveniences above read delayed from undo_action when HEY does not
say. TypeScript has no delivery conveniences: its generated operations answer the
SentMessage exactly as HEY served it, so there delayed is absent wherever HEY leaves it
out.
The generated operations change with them: CreateMessage, UpdateMessage and
CreateReply now decode their 200 answer as SentMessage (CreateMessageResponseContent,
UpdateMessageResponseContent, CreateReplyResponseContent) in every SDK. They are
marked @heyLenientSuccess (ADR-005): a 2xx whose body is empty, cannot be read or does not
decode is an empty result rather than an error, since the message has gone out — Go's
Parse*Response leaves JSON200 nil, TypeScript's data is undefined, and Rust, Kotlin and
Swift answer what {} decodes to. A refused delivery is still an error. Saving a draft
through them answers 204 with no body, which reads as that empty result; the draft's id is in
the Location header. In Go, Rust, Kotlin and Swift, save a draft through the draft
conveniences (CreateDraft, UpdateDraft, CreateReplyDraft and their equivalents), which
read that header. TypeScript has no draft conveniences: read the draft's id from the
response's Location header.
Route gains lenient_success in Rust (a struct literal needs it), and lenientSuccess in
Kotlin and Swift (defaulted to false).
Every type the crate decodes and hands back, and every enum whose set of values is HEY's to
grow, is now #[non_exhaustive]. The policy — which side a type is on and what a change to
it costs — is in rust/hey-sdk/README.md; this is what
it changes for code outside the crate.
Building one literally no longer compiles, ..Default::default() included. This reaches
tests and fixtures more than applications. The generated types and most hand-written results
keep Default, so build one and set what the test needs:
// before
let mailbox = Mailbox { name: "Imbox".into(), ..Default::default() };
// after
let mut mailbox = Mailbox::default();
mailbox.name = "Imbox".into();A few carry no Default, and a fixture for one of those comes from where the crate itself
gets it: Token, DeletedCalendar and DeletedRecording deserialize from JSON;
client::Response, FormResponse, RequestInfo and RequestResult come out of a call
against a mock server (the crate's own tests use wiremock for this); url::Match comes from
Router::recognize.
A match over an open enum needs a wildcard arm, and a struct pattern needs ..:
match error.code() {
ErrorCode::NotFound => …,
ErrorCode::RateLimit => …,
_ => …,
}Reading fields, calling methods, Clone, PartialEq and serde are untouched.
The types affected:
- Generated: every schema the model reads back and nothing the model sends. Request bodies
(
*RequestContent) and everything they mention (MessagePayload,ContactPayload,StickyPayload, …) stay literal; the generator decides by reachability from the request bodies, not by name. - Hand-written results:
WorkflowSummary,CalendarChanges,RecordingChanges,DeletedRecording,DeletedCalendar,PostingChanges,SearchResults,ContactConflict,ListedCalendar,CalendarList,services::extenzions::Extenzion,url::Match. - Runtime:
client::Response,FormResponse,oauth::Token, and what the hooks see —RequestInfoandRequestResult.OperationInfostays literal:Operation::infotakes one from a caller describing its own form-backed call.Route,RouteParamandRetrystay literal too: a caller may write a route for a path the model lacks and hand it toClient::operation. - Open enums:
ErrorCode,route::Pagination,route::ParamKind,BoxKind,StickySize,BubbleUpSlot,RepeatFrequency,CountdownUnit,TimeFormat.
Not affected, on purpose: Config, the resilience configs, ExchangeRequest,
RefreshRequest, ServerMetadata, Pkce, CachedResponse, OperationInfo, Route,
RouteParam, Retry, the *Params structs, the *Cursor structs, and the closed enums
ClearanceStatus, OccurrenceScope, RepeatUntil and ParamRole. A field added to one of
those is a 0.MINOR release.