Class: Ecoportal::API::Common::GraphQL::HttpClient

Inherits:
Common::Client
  • Object
show all
Includes:
AuthService
Defined in:
lib/ecoportal/api/common/graphql/http_client.rb

Constant Summary collapse

ENDPOINT_PATH =
'external/graphql'.freeze
CONNECT_TIMEOUT =
30
READ_TIMEOUT =
90
WRITE_TIMEOUT =
90
KEEP_ALIVE_TIMEOUT =
5

Constants included from AuthService

AuthService::DEFAULT_SERVER, AuthService::TOKEN_AUTORENEW

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from AuthService

included

Methods included from AuthService::InstanceMethods

#session_token, #session_token_renewed

Constructor Details

#initialize(email: nil, pass: nil, org_id: nil, api_key: nil, version: 'graphql', host: 'live.ecoportal.com', logger: ::Logger.new(IO::NULL), deep_logging: false, no_schema: true) ⇒ HttpClient

Returns a new instance of HttpClient.



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/ecoportal/api/common/graphql/http_client.rb', line 115

def initialize(
  email:        nil,
  pass:         nil,
  org_id:       nil,
  api_key:      nil,
  version:      'graphql',
  host:         'live.ecoportal.com',
  logger:       ::Logger.new(IO::NULL),
  deep_logging: false,
  no_schema:    true
)
  @org_id      = org_id
  @user_email  = email
  @user_pass   = pass
  @no_schema   = no_schema
  @version     = version

  super(
    api_key:      api_key,
    host:         host,
    logger:       logger,
    deep_logging: deep_logging
  )
end

Instance Attribute Details

#host ⇒ Object (readonly)

Returns the value of attribute host.



105
106
107
# File 'lib/ecoportal/api/common/graphql/http_client.rb', line 105

def host
  @host
end

#version ⇒ Object (readonly)

Returns the value of attribute version.



105
106
107
# File 'lib/ecoportal/api/common/graphql/http_client.rb', line 105

def version
  @version
end

Class Method Details

.base_url(host) ⇒ Object



90
91
92
# File 'lib/ecoportal/api/common/graphql/http_client.rb', line 90

def base_url(host)
  "#{protocol(host)}://#{host}"
end

.protocol(host) ⇒ Object



94
95
96
# File 'lib/ecoportal/api/common/graphql/http_client.rb', line 94

def protocol(host)
  host.match(/^localhost|^127\.0\.0\.1/)? 'http' : 'https'
end

Instance Method Details

#base_request ⇒ HTTP

Note:

It configures HTTP so it only allows body data in json format.

Note:

This is the ONLY place in the gem's dependency chain that talks to the server via http.rb (require 'http', pulled in by Ecoportal::API::Common::Client from the ecoportal-api gem) -- every live GraphQL query goes through #execute below, called from Logic::BaseQuery#graphql_query via client.http_client.execute, and AuthService#auth_http_client (version: 'http') reuses this same base_request for the OAuth token POST. Common::GraphQL::Client (graphlient) is a separate, unrelated path: it never reaches http.rb at all.

Without an explicit connect:, http.rb's HTTP::Timeout::PerOperation falls back to its own default of 0.25s for CONNECT even though read/write were already given explicitly (per_operation.rb: options.fetch(:connect_timeout, CONNECT_TIMEOUT), CONNECT_TIMEOUT = 0.25). connect_ssl (http/timeout/per_operation.rb) uses that same @connect_timeout for the TLS handshake via rescue_readable/ rescue_writable (http/timeout/null.rb) -- whose error text is unconditionally "Read timed out after #@read_timeout seconds", so a slow TLS handshake on the EU instance surfaced as HTTP::TimeoutError: Read timed out after 0.25 seconds even though @read_timeout itself was correctly 90 the whole time. Live-confirmed 2026-09-08 via backtrace: connect_ssl -> start_tls (http/connection.rb) -> Common::Client#post -> AuthService#session_token_data -> Common::GraphQL::Client#initialize. Read/write were never the problem; only connect was missing.

Note:

The client is made PERSISTENT (HTTP.persistent) to base_url, not a plain one-shot client. Confirmed live 2026-09-08: eu.live.ecoportal.com closes an HTTP/1.1 request carrying Connection: close -- what a non-persistent http.rb client always sends -- without ever responding. A persistent client sends Connection: keep-alive and reuses the TCP/TLS connection instead, which the SAME Ruby stack proved fine against EU. This also matches Cloudflare keep-alive hygiene; Sydney is fine either way. One HttpClient is bound to one host, so HTTP::StateError (raised only on a cross-origin request against a persistent client) cannot occur here.

Creates a HTTP object adding the X-ApiKey or X-ECOPORTAL-API-KEY param to the header, depending on the API version.

Returns:

  • (HTTP) —

    HTTP object.



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/ecoportal/api/common/graphql/http_client.rb', line 173

def base_request
  @base_request ||=
    case @version
    when NilClass, 'http'
      HTTP
    when 'v1', 'v0'
      HTTP.headers('X-ApiKey' => key_token)
    when 'v2', 'v3'
      HTTP.headers('X-ECOPORTAL-API-KEY' => key_token)
    when 'graphql'
      HTTP.headers('Authorization' => "Bearer #{session_token(host: host)}")
    end.then do |request|
      request ||= HTTP
      request.persistent(base_url, timeout: KEEP_ALIVE_TIMEOUT).accept(:json).timeout(
        connect: CONNECT_TIMEOUT,
        read:    READ_TIMEOUT,
        write:   WRITE_TIMEOUT
      )
    end
end

#execute(query_string, variables: {}, operation_name: nil) ⇒ Hash

Executes a GraphQL query or mutation over HTTP.

Parameters:

  • query_string (String) —

    full GraphQL query document.

  • variables (Hash) (defaults to: {}) —

    variable values to pass to the operation.

  • operation_name (String, nil) (defaults to: nil) —

    when the document has multiple operations.

Returns:

  • (Hash) —

    parsed response body (string-keyed, includes 'data' and optionally 'errors').

Raises:



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/ecoportal/api/common/graphql/http_client.rb', line 205

def execute(query_string, variables: {}, operation_name: nil)
  body = { query: query_string, variables: variables }
  body[:operationName] = operation_name if operation_name

  # Route through the inherited `Common::Client` pipeline
  # (`instrument` -> `with_retry` { `rate_throttling` }) so GraphQL requests
  # inherit the SAME battle-tested contingencies as the REST clients:
  # rate-limit backoff (429 / Cloudflare 1015), 5xx + connection-error retries,
  # timeouts, and instrumentation. Do NOT re-issue the request raw here — that
  # bypasses all of the above and is what let live runs die on the first 429.
  response = instrument('POST', ENDPOINT_PATH, body) do
    request { |http| http.post(url_for(''), json: body) }
  end

  handle_http_response(response)
end

#org_id ⇒ Object



194
195
196
# File 'lib/ecoportal/api/common/graphql/http_client.rb', line 194

def org_id
  @org_id || fetch_env_required('ORGANIZATION_ID')
end

#url_for(path) ⇒ String

Full URl builder of the request

Parameters:

  • path (String) —

    the tail that completes the url of the request.

Returns:

  • (String) —

    the final url.



225
226
227
# File 'lib/ecoportal/api/common/graphql/http_client.rb', line 225

def url_for(path)
  "#{api_url}#{path}"
end