Module: Restless::Har

Defined in:
lib/restless/har.rb

Overview

CONTRACT.md section 7. Captured traffic rides inside a HAR 1.2 envelope.

Class Method Summary collapse

Class Method Details

.headers_to_list(headers) ⇒ Object



43
44
45
# File 'lib/restless/har.rb', line 43

def headers_to_list(headers)
  headers.map { |name, value| { "name" => name, "value" => value } }
end

.parse_query_string(url) ⇒ Object

HAR-005. Parse the query into ordered name/value pairs.

Hand-rolled rather than delegating to a URI library so it shares the exact decoding rule with redact_url, and so a port does not have to reproduce WHATWG URL parsing to agree with us. Duplicate names are preserved, which a hash-based parse would lose.



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/restless/har.rb', line 17

def parse_query_string(url)
  q = url.index("?")
  return [] if q.nil?

  rest = url[(q + 1)..-1] || ""
  hash = rest.index("#")
  query = hash.nil? ? rest : rest[0, hash]
  return [] if query.empty?

  out = []
  query.split("&", -1).each do |pair|
    next if pair.empty?

    eq = pair.index("=")
    if eq.nil?
      out << { "name" => Redact.percent_decode(pair), "value" => "" }
    else
      out << {
        "name" => Redact.percent_decode(pair[0, eq]),
        "value" => Redact.percent_decode(pair[(eq + 1)..-1] || "")
      }
    end
  end
  out
end

.to_har_entry(captured) ⇒ Object

captured is a hash with string keys:

requestId, startedAt, duration, routePattern?,
request: {method, url, headers, body?}, response: {status, headers, body?}

A nil body means "no body captured" and is distinct from an empty one: HAR-011 makes the first report -1 and the second 0.



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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/restless/har.rb', line 53

def to_har_entry(captured)
  request = captured["request"] || {}
  response = captured["response"] || {}
  request_headers = request["headers"] || {}
  response_headers = response["headers"] || {}

  request_body = request["body"]
  response_body = response["body"]

  req_content_type = request_headers["content-type"]
  req_content_type = "" if req_content_type.nil? || req_content_type == ""
  # HAR-007: mimeType falls back when the response has no content type.
  res_content_type = response_headers["content-type"]
  if res_content_type.nil? || res_content_type == ""
    res_content_type = "application/octet-stream"
  end

  duration = captured["duration"] || 0

  har_request = {
    "method" => request["method"],
    "url" => request["url"],
    "httpVersion" => "HTTP/1.1", # HAR-003 (reserved)
    "headers" => headers_to_list(request_headers), # HAR-004
    "queryString" => parse_query_string(request["url"].to_s)
  }
  # HAR-006: postData is present ONLY when a request body was captured.
  # The reference tests truthiness, so an empty-string body produces no
  # postData but still reports bodySize 0.
  unless request_body.nil? || request_body.empty?
    har_request["postData"] = {
      "mimeType" => req_content_type,
      "text" => request_body
    }
  end
  har_request["headersSize"] = -1 # HAR-012
  har_request["bodySize"] =
    request_body.nil? ? -1 : Text.utf8_length(request_body) # HAR-010/011

  {
    "startedDateTime" => captured["startedAt"], # HAR-001
    "time" => duration, # HAR-002
    "request" => har_request,
    "response" => {
      "status" => response["status"],
      "statusText" => "", # HAR-008 (reserved)
      "httpVersion" => "HTTP/1.1",
      "headers" => headers_to_list(response_headers),
      "content" => {
        "size" => response_body.nil? ? 0 : Text.utf8_length(response_body),
        "mimeType" => res_content_type,
        "text" => response_body.nil? ? "" : response_body
      },
      "headersSize" => -1,
      "bodySize" => response_body.nil? ? -1 : Text.utf8_length(response_body)
    },
    "timings" => { "send" => 0, "wait" => duration, "receive" => 0 }
  }
end