diff --git a/README.md b/README.md index cc1763d7..dd28ff29 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ accurate and fully typed. - [Advanced Usage](#advanced-usage) - [Additional Options](#additional-options) - [Setting the endpoint](#setting-the-endpoint) + - [Setting the request timeout](#setting-the-request-timeout) - [Configuring the Faraday Client](#configuring-the-faraday-client) - [Using the Faraday Client](#using-the-faraday-client) - [Overriding the Client](#overriding-the-client) @@ -381,6 +382,7 @@ the constructor takes some advanced options that affect behavior. seam = Seam.new( api_key: "your-api-key", endpoint: "https://example.com", + timeout: 30, faraday_options: {}, faraday_retry_options: {} ) @@ -392,6 +394,7 @@ these options may be passed in as keyword arguments. ```ruby seam = Seam.from_api_key("some-api-key", endpoint: "https://example.com", + timeout: 30, faraday_options: {}, faraday_retry_options: {}) ``` @@ -403,6 +406,27 @@ e.g., testing or proxy setups. This option corresponds to the [Faraday](https:// Either pass the `endpoint` option, or set the `SEAM_ENDPOINT` environment variable. +#### Setting the request timeout + +Requests time out after 30 seconds by default. +Pass the `timeout` option, in seconds, to override this: + +```ruby +seam = Seam.new(api_key: "your-api-key", timeout: 60) +``` + +The value sets both the Faraday `timeout` and `open_timeout`. +Either may be set independently via `faraday_options`, which takes precedence: + +```ruby +seam = Seam.new( + api_key: "your-api-key", + faraday_options: {request: {timeout: 60, open_timeout: 5}} +) +``` + +A request that exceeds the timeout raises `Faraday::TimeoutError`. + #### Configuring the Faraday Client The Faraday client and retry behavior may be configured with custom initiation options diff --git a/lib/seam.rb b/lib/seam.rb index f958acb4..796386c4 100644 --- a/lib/seam.rb +++ b/lib/seam.rb @@ -11,12 +11,13 @@ def self.new(**args) Seam::Http.new(**args) end - def self.from_api_key(api_key, endpoint: nil, wait_for_action_attempt: true) - Seam::Http.from_api_key(api_key, endpoint: endpoint, wait_for_action_attempt: wait_for_action_attempt) + def self.from_api_key(api_key, endpoint: nil, wait_for_action_attempt: true, timeout: nil) + Seam::Http.from_api_key(api_key, endpoint: endpoint, wait_for_action_attempt: wait_for_action_attempt, + timeout: timeout) end - def self.from_personal_access_token(personal_access_token, workspace_id, endpoint: nil, wait_for_action_attempt: true) - Seam::Http.from_personal_access_token(personal_access_token, workspace_id, endpoint: endpoint, wait_for_action_attempt: wait_for_action_attempt) + def self.from_personal_access_token(personal_access_token, workspace_id, endpoint: nil, wait_for_action_attempt: true, timeout: nil) + Seam::Http.from_personal_access_token(personal_access_token, workspace_id, endpoint: endpoint, wait_for_action_attempt: wait_for_action_attempt, timeout: timeout) end def self.lts_version diff --git a/lib/seam/default_endpoint.rb b/lib/seam/defaults.rb similarity index 80% rename from lib/seam/default_endpoint.rb rename to lib/seam/defaults.rb index 74d4f254..b6da11e1 100644 --- a/lib/seam/default_endpoint.rb +++ b/lib/seam/defaults.rb @@ -2,4 +2,6 @@ module Seam DEFAULT_ENDPOINT = "https://connect.getseam.com" + + DEFAULT_TIMEOUT = 30 end diff --git a/lib/seam/http.rb b/lib/seam/http.rb index 67c1a47a..497e214f 100644 --- a/lib/seam/http.rb +++ b/lib/seam/http.rb @@ -8,13 +8,14 @@ def self.new(**args) Http::SingleWorkspace.new(**args) end - def self.from_api_key(api_key, endpoint: nil, wait_for_action_attempt: true) - Http::SingleWorkspace.from_api_key(api_key, endpoint: endpoint, wait_for_action_attempt: wait_for_action_attempt) + def self.from_api_key(api_key, endpoint: nil, wait_for_action_attempt: true, timeout: nil) + Http::SingleWorkspace.from_api_key(api_key, endpoint: endpoint, wait_for_action_attempt: wait_for_action_attempt, + timeout: timeout) end - def self.from_personal_access_token(personal_access_token, workspace_id, endpoint: nil, wait_for_action_attempt: true) + def self.from_personal_access_token(personal_access_token, workspace_id, endpoint: nil, wait_for_action_attempt: true, timeout: nil) Http::SingleWorkspace.from_personal_access_token(personal_access_token, workspace_id, endpoint: endpoint, - wait_for_action_attempt: wait_for_action_attempt) + wait_for_action_attempt: wait_for_action_attempt, timeout: timeout) end class ApiError < StandardError diff --git a/lib/seam/http_multi_workspace.rb b/lib/seam/http_multi_workspace.rb index e578f63f..df2c2ca0 100644 --- a/lib/seam/http_multi_workspace.rb +++ b/lib/seam/http_multi_workspace.rb @@ -14,14 +14,14 @@ module Http class MultiWorkspace attr_reader :client, :defaults - def initialize(personal_access_token:, endpoint: nil, wait_for_action_attempt: true, faraday_options: {}, - faraday_retry_options: {}) + def initialize(personal_access_token:, endpoint: nil, wait_for_action_attempt: true, timeout: nil, + faraday_options: {}, faraday_retry_options: {}) @wait_for_action_attempt = wait_for_action_attempt @defaults = {"wait_for_action_attempt" => wait_for_action_attempt} @endpoint = Http::Options.get_endpoint(endpoint) @auth_headers = Http::Auth.get_auth_headers_for_multi_workspace_personal_access_token(personal_access_token) @client = Http::Request.create_faraday_client(@endpoint, @auth_headers, faraday_options, - faraday_retry_options) + faraday_retry_options, timeout: timeout) end def self.lts_version @@ -36,11 +36,12 @@ def workspaces @workspaces ||= WorkspacesProxy.new(Seam::Clients::Workspaces.new(client: @client, defaults: @defaults)) end - def self.from_personal_access_token(personal_access_token, endpoint: nil, wait_for_action_attempt: true, faraday_options: {}, faraday_retry_options: {}) + def self.from_personal_access_token(personal_access_token, endpoint: nil, wait_for_action_attempt: true, timeout: nil, faraday_options: {}, faraday_retry_options: {}) new( personal_access_token: personal_access_token, endpoint: endpoint, wait_for_action_attempt: wait_for_action_attempt, + timeout: timeout, faraday_options: faraday_options, faraday_retry_options: faraday_retry_options ) diff --git a/lib/seam/http_single_workspace.rb b/lib/seam/http_single_workspace.rb index 58466196..ce3ad60c 100644 --- a/lib/seam/http_single_workspace.rb +++ b/lib/seam/http_single_workspace.rb @@ -17,7 +17,7 @@ class SingleWorkspace attr_reader :client, :defaults def initialize(client: nil, api_key: nil, personal_access_token: nil, workspace_id: nil, endpoint: nil, - wait_for_action_attempt: true, faraday_options: {}, faraday_retry_options: {}) + wait_for_action_attempt: true, timeout: nil, faraday_options: {}, faraday_retry_options: {}) @defaults = Seam::DeepHashAccessor.new({"wait_for_action_attempt" => wait_for_action_attempt}) # A client carries its own endpoint and authorization, so the auth @@ -28,7 +28,8 @@ def initialize(client: nil, api_key: nil, personal_access_token: nil, workspace_ @endpoint = options[:endpoint] @auth_headers = options[:auth_headers] - Http::Request.create_faraday_client(@endpoint, @auth_headers, faraday_options, faraday_retry_options) + Http::Request.create_faraday_client(@endpoint, @auth_headers, faraday_options, faraday_retry_options, + timeout: timeout) end initialize_routes(client: @client, defaults: @defaults) @@ -42,14 +43,14 @@ def create_paginator(request, params = {}) Paginator.new(request, params) end - def self.from_api_key(api_key, endpoint: nil, wait_for_action_attempt: true, faraday_options: {}, faraday_retry_options: {}) + def self.from_api_key(api_key, endpoint: nil, wait_for_action_attempt: true, timeout: nil, faraday_options: {}, faraday_retry_options: {}) new(api_key: api_key, endpoint: endpoint, wait_for_action_attempt: wait_for_action_attempt, - faraday_options: faraday_options, faraday_retry_options: faraday_retry_options) + timeout: timeout, faraday_options: faraday_options, faraday_retry_options: faraday_retry_options) end - def self.from_personal_access_token(personal_access_token, workspace_id, endpoint: nil, wait_for_action_attempt: true, faraday_options: {}, faraday_retry_options: {}) + def self.from_personal_access_token(personal_access_token, workspace_id, endpoint: nil, wait_for_action_attempt: true, timeout: nil, faraday_options: {}, faraday_retry_options: {}) new(personal_access_token: personal_access_token, workspace_id: workspace_id, endpoint: endpoint, - wait_for_action_attempt: wait_for_action_attempt, faraday_options: faraday_options, faraday_retry_options: faraday_retry_options) + wait_for_action_attempt: wait_for_action_attempt, timeout: timeout, faraday_options: faraday_options, faraday_retry_options: faraday_retry_options) end end end diff --git a/lib/seam/options.rb b/lib/seam/options.rb index 4a1821c3..f3ef5e83 100644 --- a/lib/seam/options.rb +++ b/lib/seam/options.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative "default_endpoint" +require_relative "defaults" module Seam module Http diff --git a/lib/seam/request.rb b/lib/seam/request.rb index bb89e80e..6d4e5423 100644 --- a/lib/seam/request.rb +++ b/lib/seam/request.rb @@ -2,6 +2,7 @@ require "faraday" require "faraday/retry" +require_relative "defaults" require_relative "lts_version" require_relative "version" require_relative "paginator" @@ -9,10 +10,15 @@ module Seam module Http module Request - def self.create_faraday_client(endpoint, auth_headers, faraday_options = {}, faraday_retry_options = {}) + def self.create_faraday_client(endpoint, auth_headers, faraday_options = {}, faraday_retry_options = {}, + timeout: nil) + timeout ||= Seam::DEFAULT_TIMEOUT + default_options = { url: endpoint, - headers: auth_headers.merge(default_headers) + headers: auth_headers.merge(default_headers), + # open_timeout bounds the connect phase, which timeout does not cover. + request: {timeout: timeout, open_timeout: timeout} } options = deep_merge(default_options, faraday_options) diff --git a/spec/seam_client/faraday_options_spec.rb b/spec/seam_client/faraday_options_spec.rb index 6e7207d1..d882683b 100644 --- a/spec/seam_client/faraday_options_spec.rb +++ b/spec/seam_client/faraday_options_spec.rb @@ -8,12 +8,12 @@ endpoint: endpoint, faraday_options: { headers: {"Custom-Header" => "Test-Value"}, - request: {timeout: 30} + request: {timeout: 45} } ) expect(seam.client.headers["Custom-Header"]).to eq("Test-Value") - expect(seam.client.options.timeout).to eq(30) + expect(seam.client.options.timeout).to eq(45) end it "keeps the auth and SDK headers when custom headers are given" do diff --git a/spec/seam_client/timeout_spec.rb b/spec/seam_client/timeout_spec.rb new file mode 100644 index 00000000..f725b7af --- /dev/null +++ b/spec/seam_client/timeout_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +RSpec.describe Seam::Http::SingleWorkspace, :fake do + describe "timeout" do + it "defaults to 30 seconds" do + seam = described_class.new(api_key: seed["seam_apikey1_token"], endpoint: endpoint) + + expect(seam.client.options.timeout).to eq(30) + expect(seam.client.options.open_timeout).to eq(30) + end + + it "can be overridden with the timeout option" do + seam = described_class.new(api_key: seed["seam_apikey1_token"], endpoint: endpoint, timeout: 60) + + expect(seam.client.options.timeout).to eq(60) + expect(seam.client.options.open_timeout).to eq(60) + end + + it "can be overridden by the factory methods" do + seam = described_class.from_api_key(seed["seam_apikey1_token"], endpoint: endpoint, timeout: 60) + + expect(seam.client.options.timeout).to eq(60) + end + + it "gives faraday_options the last word" do + seam = described_class.new( + api_key: seed["seam_apikey1_token"], + endpoint: endpoint, + timeout: 60, + faraday_options: {request: {timeout: 5}} + ) + + expect(seam.client.options.timeout).to eq(5) + expect(seam.client.options.open_timeout).to eq(60) + end + + it "does not apply to a client passed in by the caller" do + client = Faraday.new(url: endpoint) + seam = described_class.new(client: client) + + expect(seam.client.options.timeout).to be_nil + end + end + + describe "a request that exceeds the timeout" do + let(:url) { "#{Seam::DEFAULT_ENDPOINT}/devices/list" } + + it "raises Faraday::TimeoutError" do + stub_request(:post, url).to_timeout + + seam = Seam.new(api_key: "seam_some_api_key", timeout: 1) + + expect { seam.devices.list }.to raise_error(Faraday::ConnectionFailed) + end + end +end