Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,10 @@ organization = WorkOS.client.organizations.get_organization(
)
```

`Idempotency-Key` is only sent on `POST`, `PUT`, and `PATCH` requests — either when you provide `request_options[:idempotency_key]`, or auto-generated when the SDK retries one of those requests after a transient failure. It is never attached to `GET` or `DELETE` requests.
`Idempotency-Key` is only sent on `POST`, `PUT`, and `PATCH` requests, and never on `GET` or `DELETE`. Every `POST` carries one: the key you provide in `request_options[:idempotency_key]`, or one the SDK generates before the first attempt and reuses on every retry. `PUT` and `PATCH` requests carry one when you provide it, or when the SDK retries them after a transient failure.

> [!NOTE]
> The WorkOS API currently honors `Idempotency-Key` only on the [Create Audit Log Event](https://workos.com/docs/reference/audit-logs/event) endpoint (`audit_logs.create_event`). Other endpoints accept the header but do not deduplicate requests, so a retried mutation elsewhere can still create a duplicate.
> The WorkOS API currently honors `Idempotency-Key` only on the [Create Audit Log Event](https://workos.com/docs/reference/audit-logs/event) endpoint (`audit_logs.create_event`) and on Pipes explicit adds (`pipes.create_data_integration_api_key`, `pipes.create_data_integration_client_credential`, and `pipes.create_user_connected_account` / `pipes.create_organization_connected_account` with `connection_intent: "add"`). Other endpoints accept the header but do not deduplicate requests, so a retried mutation elsewhere can still create a duplicate.

## Usage Examples

Expand Down
3 changes: 3 additions & 0 deletions lib/workos/base_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ def post_request(path:, auth: false, body: {}, params: {}, request_options: nil)
req.body = body.nil? ? "" : body.to_json
req["Content-Type"] = "application/json"
inject_idempotency_key(req, request_options)
# Keyed before the first attempt, so a retry after a lost response
# carries the key the server already saw.
req["Idempotency-Key"] ||= SecureRandom.uuid
req
end

Expand Down
7 changes: 4 additions & 3 deletions test/workos/test_base_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -164,15 +164,16 @@ def test_delete_request_serializes_explicit_nil_as_json_null
assert_equal({"external_id" => nil}, JSON.parse(request.body))
end

def test_retry_path_generates_idempotency_key_for_mutating_requests
def test_post_sends_one_idempotency_key_on_the_first_attempt_and_its_retries
stub_request(:post, "https://api.workos.com/widgets")
.to_return({status: 500, body: '{"message":"retry"}'}, {status: 200, body: "{}"})

@client.singleton_class.define_method(:sleep) { |_duration| nil }
@client.request(method: :post, path: "/widgets", body: {name: "widget"})

assert_requested(:post, "https://api.workos.com/widgets", times: 2)
assert_requested(:post, "https://api.workos.com/widgets", headers: {"Idempotency-Key" => /.+/}, times: 1)
assert_requested(:post, "https://api.workos.com/widgets", headers: {"Idempotency-Key" => /.+/}, times: 2)
keys = WebMock::RequestRegistry.instance.requested_signatures.hash.keys.map { |signature| signature.headers["Idempotency-Key"] }
assert_equal 1, keys.uniq.size
end

def test_409_idempotency_error_raises_specific_error
Expand Down
Loading