Class: GraphWeaver::Transport

Inherits:
Object
  • Object
show all
Extended by:
T::Helpers, T::Sig
Defined in:
lib/graph_weaver/transport.rb,
lib/graph_weaver/transport/http.rb,
lib/graph_weaver/transport/faraday.rb

Overview

Base class for the bundled network transports — Transport::HTTP (zero-dependency net/http, loaded by default) and Transport::Faraday (opt-in). A transport speaks GraphQL-over-HTTP and satisfies the same execute(query, variables:) => => ..., "errors" => ... contract as a schema class or a fake — anything in a client slot.

The base class owns the shared flow — encode the request, reclassify network-level failures as TransportError, raise ServerError on a non-2xx status, parse the body — so a subclass only implements post: take the request body, return [status, body].

Defined Under Namespace

Classes: Faraday, HTTP

Constant Summary collapse

REQUEST_MUTEX =

"[req 3 FilteredPokemon]" — a per-process request id plus the operation name (when the document declares one)

Mutex.new
LOG_QUERY_LIMIT =

keep debug readable: a 100-line introspection query would drown the log — the INFO introspection line already carries the timing

600

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#urlObject (readonly)

the endpoint this transport talks to — recorded into cached schema dumps as provenance (see SchemaLoader.introspect)



26
27
28
# File 'lib/graph_weaver/transport.rb', line 26

def url
  @url
end

Class Method Details

.log_tag(query) ⇒ Object



103
104
105
106
107
# File 'lib/graph_weaver/transport.rb', line 103

def self.log_tag(query)
  id = REQUEST_MUTEX.synchronize { @request_count = (@request_count || 0) + 1 }
  name = query[/\A\s*(?:query|mutation|subscription)\s+([A-Za-z_]\w*)/, 1]
  "[req #{id}#{" #{name}" if name}]"
end

.truncate_for_log(query) ⇒ Object



112
113
114
115
116
# File 'lib/graph_weaver/transport.rb', line 112

def self.truncate_for_log(query)
  return query if query.length <= LOG_QUERY_LIMIT

  "#{query[0, LOG_QUERY_LIMIT]}... (truncated, #{query.bytesize} bytes total)"
end

Instance Method Details

#execute(query, variables: {}) ⇒ Object



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/graph_weaver/transport.rb', line 28

def execute(query, variables: {})
  # tag pairs this request's log lines (threads interleave), and names
  # the operation so the log says WHICH query, not just the url
  tag = GraphWeaver.logger && GraphWeaver::Transport.log_tag(query)

  # full query + variables at debug only — they can carry PII
  GraphWeaver.log(:debug) do
    "POST #{url} #{tag} variables=#{JSON.generate(variables)}\n#{GraphWeaver::Transport.truncate_for_log(query)}"
  end

  encoded = begin
    JSON.generate(query:, variables:)
  rescue JSON::GeneratorError => e
    # a value with no JSON form (NaN, Infinity, binary) — the caller's
    # bug, surfaced under the umbrella instead of a raw JSON:: error
    raise GraphWeaver::Error, "variables are not JSON-serializable: #{e.message}"
  end

  status, body = begin
    GraphWeaver.log_timed(:debug, "POST #{url} #{tag} completed") do
      post(encoded)
    end
  rescue *GraphWeaver.transport_errors.to_a => e
    # never got a response — DNS, connection refused/reset, TLS, timeout
    raise GraphWeaver::TransportError, "#{e.class}: #{e.message}"
  end

  GraphWeaver.log(:debug) { "HTTP #{status} #{tag} from #{url} (#{body.to_s.bytesize} bytes)" }

  parsed = parse_body(body)

  # reached the server, but it returned a non-2xx status. Per
  # graphql-over-http, routers (Apollo Server/Router) send request
  # errors as 4xx WITH a GraphQL errors body — those flow into the
  # envelope so QueryError machinery sees the structured errors; only
  # a body that isn't GraphQL (proxy pages, HTML 500s) is a ServerError.
  unless (200..299).cover?(status)
    # only a body carrying actual GraphQL errors flows through — a 4xx with
    # `"errors": null` (or []) isn't a structured error response, so the
    # status stays the signal
    return parsed if parsed.is_a?(Hash) && parsed["errors"].is_a?(Array) && parsed["errors"].any?

    raise GraphWeaver::ServerError.new(status:, body: body.to_s)
  end

  unless parsed.is_a?(Hash)
    # a 200 that isn't a GraphQL object — an HTML error page from a proxy, a
    # captive portal, or a bare JSON array/string: the server misbehaved
    raise GraphWeaver::ServerError.new(status:, body: "non-GraphQL response: #{body.to_s[0, 500]}")
  end

  parsed
end

#inspectObject Also known as: to_s

never leak Authorization headers through logs/exceptions — a transport inspects as its class + endpoint, nothing more



94
95
96
# File 'lib/graph_weaver/transport.rb', line 94

def inspect
  "#<#{self.class.name} url=#{url.inspect}>"
end