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



100
101
102
103
104
# File 'lib/graph_weaver/transport.rb', line 100

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



109
110
111
112
113
# File 'lib/graph_weaver/transport.rb', line 109

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
# 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)
    return parsed if parsed.is_a?(Hash) && parsed.key?("errors")

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

  unless parsed
    # a 200 that isn't GraphQL — an HTML error page from a proxy, a
    # captive portal: the server misbehaved, classify it that way
    raise GraphWeaver::ServerError.new(status:, body: "non-JSON 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



91
92
93
# File 'lib/graph_weaver/transport.rb', line 91

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