1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 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 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 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 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
|
# frozen_string_literal: true
require "net/http"
require "zlib"
module Sentry
class HTTPTransport < Transport
GZIP_ENCODING = "gzip"
GZIP_THRESHOLD = 1024 * 30
CONTENT_TYPE = 'application/x-sentry-envelope'
DEFAULT_DELAY = 60
RETRY_AFTER_HEADER = "retry-after"
RATE_LIMIT_HEADER = "x-sentry-rate-limits"
USER_AGENT = "sentry-ruby/#{Sentry::VERSION}"
def initialize(*args)
super
@endpoint = @dsn.envelope_endpoint
log_debug("Sentry HTTP Transport will connect to #{@dsn.server}")
end
def send_data(data)
encoding = ""
if should_compress?(data)
data = Zlib.gzip(data)
encoding = GZIP_ENCODING
end
headers = {
'Content-Type' => CONTENT_TYPE,
'Content-Encoding' => encoding,
'X-Sentry-Auth' => generate_auth_header,
'User-Agent' => USER_AGENT
}
response = conn.start do |http|
request = ::Net::HTTP::Post.new(@endpoint, headers)
request.body = data
http.request(request)
end
if response.code.match?(/\A2\d{2}/)
if has_rate_limited_header?(response)
handle_rate_limited_response(response)
end
else
error_info = "the server responded with status #{response.code}"
if response.code == "429"
handle_rate_limited_response(response)
else
error_info += "\nbody: #{response.body}"
error_info += " Error in headers is: #{response['x-sentry-error']}" if response['x-sentry-error']
end
raise Sentry::ExternalError, error_info
end
rescue SocketError => e
raise Sentry::ExternalError.new(e.message)
end
private
def has_rate_limited_header?(headers)
headers[RETRY_AFTER_HEADER] || headers[RATE_LIMIT_HEADER]
end
def handle_rate_limited_response(headers)
rate_limits =
if rate_limits = headers[RATE_LIMIT_HEADER]
parse_rate_limit_header(rate_limits)
elsif retry_after = headers[RETRY_AFTER_HEADER]
# although Sentry doesn't send a date string back
# based on HTTP specification, this could be a date string (instead of an integer)
retry_after = retry_after.to_i
retry_after = DEFAULT_DELAY if retry_after == 0
{ nil => Time.now + retry_after }
else
{ nil => Time.now + DEFAULT_DELAY }
end
rate_limits.each do |category, limit|
if current_limit = @rate_limits[category]
if current_limit < limit
@rate_limits[category] = limit
end
else
@rate_limits[category] = limit
end
end
end
def parse_rate_limit_header(rate_limit_header)
time = Time.now
result = {}
limits = rate_limit_header.split(",")
limits.each do |limit|
next if limit.nil? || limit.empty?
begin
retry_after, categories = limit.strip.split(":").first(2)
retry_after = time + retry_after.to_i
categories = categories.split(";")
if categories.empty?
result[nil] = retry_after
else
categories.each do |category|
result[category] = retry_after
end
end
rescue StandardError
end
end
result
end
def should_compress?(data)
@transport_configuration.encoding == GZIP_ENCODING && data.bytesize >= GZIP_THRESHOLD
end
def conn
server = URI(@dsn.server)
connection =
if proxy = normalize_proxy(@transport_configuration.proxy)
::Net::HTTP.new(server.hostname, server.port, proxy[:uri].hostname, proxy[:uri].port, proxy[:user], proxy[:password])
else
::Net::HTTP.new(server.hostname, server.port, nil)
end
connection.use_ssl = server.scheme == "https"
connection.read_timeout = @transport_configuration.timeout
connection.write_timeout = @transport_configuration.timeout if connection.respond_to?(:write_timeout)
connection.open_timeout = @transport_configuration.open_timeout
ssl_configuration.each do |key, value|
connection.send("#{key}=", value)
end
connection
end
def normalize_proxy(proxy)
return proxy unless proxy
case proxy
when String
uri = URI(proxy)
{ uri: uri, user: uri.user, password: uri.password }
when URI
{ uri: proxy, user: proxy.user, password: proxy.password }
when Hash
proxy
end
end
def ssl_configuration
configuration = {
verify: @transport_configuration.ssl_verification,
ca_file: @transport_configuration.ssl_ca_file
}.merge(@transport_configuration.ssl || {})
configuration[:verify_mode] = configuration.delete(:verify) ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
configuration
end
end
end
|