Class: Transport::HttpGraphqlApiConnection
- Inherits:
-
Object
- Object
- Transport::HttpGraphqlApiConnection
- Defined in:
- app/services/transport/http_graphql_api_connection.rb
Constant Summary collapse
- VALID_HTTP_METHODS =
%w[get put post patch delete head]
Instance Attribute Summary collapse
-
#logger ⇒ Object
readonly
Returns the value of attribute logger.
-
#profile ⇒ Object
readonly
Returns the value of attribute profile.
Instance Method Summary collapse
- #build_request_headers ⇒ Object
-
#fetch_token ⇒ String
Function used to get an authentication token based on a client's id and secret.
- #fetch_url_to_upload(url, file_name, category, resource) ⇒ Object
-
#initialize(options = {}) ⇒ HttpGraphqlApiConnection
constructor
A new instance of HttpGraphqlApiConnection.
- #send_data(data) ⇒ Object
-
#send_graphql_request(token, query, variables) ⇒ String
Function that formats the HTTP Graphql request with the needed headers and correct Graphql mutation or query payload to send to the Wayfair api.
-
#send_request(method, url, body = nil, headers = nil) ⇒ HTTP.rb Response
Helper function to send an HTTP request.
- #successful?(http_res) ⇒ Boolean
Constructor Details
#initialize(options = {}) ⇒ HttpGraphqlApiConnection
Returns a new instance of HttpGraphqlApiConnection.
8 9 10 11 12 13 14 15 16 17 |
# File 'app/services/transport/http_graphql_api_connection.rb', line 8 def initialize( = {}) @options = # if there's a profile, grab the headers to send from it if (profile = @options[:profile]) @profile = Heatwave::Configuration.fetch(profile&.to_sym) elsif @options[:profile_attributes] @profile = @options[:profile_attributes] end @logger = [:logger] || Rails.logger end |
Instance Attribute Details
#logger ⇒ Object (readonly)
Returns the value of attribute logger.
6 7 8 |
# File 'app/services/transport/http_graphql_api_connection.rb', line 6 def logger @logger end |
#profile ⇒ Object (readonly)
Returns the value of attribute profile.
6 7 8 |
# File 'app/services/transport/http_graphql_api_connection.rb', line 6 def profile @profile end |
Instance Method Details
#build_request_headers ⇒ Object
34 35 36 37 38 39 40 |
# File 'app/services/transport/http_graphql_api_connection.rb', line 34 def build_request_headers { 'content-type': 'application/json', 'cache-control': 'no-cache', authorization: format('Bearer %s', fetch_token) }.stringify_keys end |
#fetch_token ⇒ String
Function used to get an authentication token based on a client's id and secret. The token will later
be passed into the authentication header for the HTTP request in the format of "Bearer TOKEN". If the
request throws an exception or the user can't be authenticated, then the function will return None and
the error will be printed out to the console.
Official Wayfair Documentation: https://developer.wayfair.com/docs/#authentication
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 |
# File 'app/services/transport/http_graphql_api_connection.rb', line 135 def fetch_token # Endpoint to authenticate for Wayfair’s API # Determine audience url based on the authentication url uri = Addressable::URI.parse(@profile[:api_url]) audience = "#{uri.scheme}//#{uri.host}" auth_payload = { grant_type: 'client_credentials', client_id: @profile[:client_id], client_secret: @profile[:client_secret], audience: }.to_json # auth_payload = %{{ # "grant_type": "client_credentials", # "client_id": "%s", # "client_secret": "%s", # "audience": "https://api.wayfair.com/" # }} % [@profile[:client_id], @profile[:client_secret]] headers = { 'content-type': 'application/json', 'cache-control': 'no-cache' }.stringify_keys num_attempts = 0 begin num_attempts += 1 res = send_request('POST', @profile[:auth_url], auth_payload, headers) response = res[:http_res] # Basic guard raise StandardError, "Wayfair auth response was nil on attempt #{num_attempts}" if response.nil? # Honor explicit 429 backoff from server if response.status == 429 retry_after = response.headers['Retry-After']&.to_i raise HTTP::RateLimitExceededError.new( status_code: 429, headers: response.headers, retry_after: retry_after, message: "429 from Wayfair auth; headers: #{response.headers.inspect} on attempt #{num_attempts}, retry_after=#{retry_after}" ) end # Retry on 5xx server errors raise StandardError, "Wayfair auth non-success status #{response.code} on attempt #{num_attempts}" if response.code && response.code >= 500 body_text = response.body.to_s token = JSON.parse(body_text)['access_token'] raise JSON::ParserError, 'Missing access_token in Wayfair auth response' if token.blank? token rescue HTTP::RateLimitExceededError => e logger.error e if num_attempts.positive? && e raise unless num_attempts <= 5 sleep_seconds = e.retry_after.to_i if sleep_seconds.positive? && sleep_seconds <= 60 sleep(sleep_seconds) else sleep(15 + (15 * num_attempts)) end retry rescue JSON::ParserError, *(Retryable::TIMEOUT_CLASSES - [HTTP::RateLimitExceededError]) => e # Defensive: send_request already retries timeouts; also retry parse errors for empty/invalid JSON logger.error e if num_attempts.positive? && e raise unless num_attempts <= 5 sleep(15 + (15 * num_attempts)) retry rescue StandardError => e # Retry generic 5xx-triggered errors; do not retry other 4xx (invalid creds, etc.) raise unless num_attempts <= 5 && e..to_s.include?('non-success status') logger.error e if num_attempts.positive? sleep(15 + (15 * num_attempts)) retry end end |
#fetch_url_to_upload(url, file_name, category, resource) ⇒ Object
42 43 44 |
# File 'app/services/transport/http_graphql_api_connection.rb', line 42 def fetch_url_to_upload(url, file_name, category, resource) Upload.uploadify_from_url(file_name:, url:, category:, resource:, headers: build_request_headers) end |
#send_data(data) ⇒ Object
19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
# File 'app/services/transport/http_graphql_api_connection.rb', line 19 def send_data(data) t = fetch_token res = send_graphql_request(t, data, 'null') http_res = res[:http_res] if data.index('inventory') && !successful?(http_res) # here (especially with inventory) it will fail often on the first use of a token, # so send request again using the same token res = send_graphql_request(t, data, 'null') http_res = res[:http_res] end attempt_number_reached = res[:attempt_number_reached] logger.debug("GraphQL request complete", method: 'POST', status: http_res&.code, attempts: attempt_number_reached) { success: successful?(http_res), http_result: http_res, attempt_number_reached: attempt_number_reached } end |
#send_graphql_request(token, query, variables) ⇒ String
Function that formats the HTTP Graphql request with the needed headers and correct Graphql mutation or query
payload to send to the Wayfair api. The payload is a string in the format
: {Graphql query or mutation string, "variables": or mutation variables JSON} which is generated from the query and variables inputs.
112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'app/services/transport/http_graphql_api_connection.rb', line 112 def send_graphql_request(token, query, variables) # TODO: change this to prod when going live # Wayfair’s GraphQL API endpoint headers = { 'content-type': 'application/json', 'cache-control': 'no-cache', authorization: format('Bearer %s', token) } # Payload must be formatted as valid JSON, see https://www.json.org/ graphql_payload = format(%({"query": "%s", "variables": %s}), query, variables) send_request('POST', @profile[:api_url], graphql_payload, headers) end |
#send_request(method, url, body = nil, headers = nil) ⇒ HTTP.rb Response
Helper function to send an HTTP request.
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'app/services/transport/http_graphql_api_connection.rb', line 78 def send_request(method, url, body = nil, headers = nil) body ||= '' # Parse full URI into object with accessible components url = URI.parse(url) # Initialize and configure HTTP request client # Configure request object given method http_client = HTTP.headers(headers).timeout(180) http_res = nil attempt_number_reached = 0 Retryable.retryable(tries: 3, sleep: lambda { |n| 4**n }, on: Retryable::TIMEOUT_CLASSES) do |attempt_number, _exception| http_res = case method when 'POST' http_client.post(url, body:) when 'GET' http_client.get(url) end attempt_number_reached = attempt_number + 1 end # Send request return response logger.debug("Transport::HttpGraphqlApiConnection send_request", url: url.to_s, method: method, attempts: attempt_number_reached, status: http_res&.code) # https.request(request) { http_res: http_res, attempt_number_reached: attempt_number_reached } end |
#successful?(http_res) ⇒ Boolean
46 47 48 49 50 51 52 53 54 55 56 |
# File 'app/services/transport/http_graphql_api_connection.rb', line 46 def successful?(http_res) logger.info "successful? http_res.body: #{http_res.body}" return false unless http_res.status.success? data = begin JSON.parse(http_res.body).with_indifferent_access rescue StandardError {} end data[:errors].blank? end |