twingly-http 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c96f42ded2e94d947bd2a15cc7de75669e3f2dc06a17ff7d648eb3c0cf862c5e
4
+ data.tar.gz: 5203e3833efe611708b1b185708735b2c6a027d20d8edd0e79c80dcca696d73a
5
+ SHA512:
6
+ metadata.gz: 6869f39208327e4b834f999e96a1a0d7253768022aca54f259c1f0cf57bf60331e4aabc966919ad0a7bef08e3091ff7090c9e576986f7d4f5336894ac47b250b
7
+ data.tar.gz: 4cf992e7c60229b8fcbfd3ab62b0584e8531b229fd8a863d8ab1a47b248c5c2697eb225beecb6ec26377bce50009bf6ba295e1fee45622951be2809ffb5b4640
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Twingly AB
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # Twingly::HTTP
2
+
3
+ Robust HTTP client
4
+
5
+
6
+ ## Release workflow
7
+
8
+ * Bump the version in `lib/twingly/version.rb` in a commit, no need to push (the release task does that).
9
+
10
+ * Ensure you are signed in to RubyGems.org as [twingly][twingly-rubygems] with `gem signin`.
11
+
12
+ * Build and [publish](http://guides.rubygems.org/publishing/) the gem. This will create the proper tag in git, push the commit and tag and upload to RubyGems.
13
+
14
+ bundle exec rake release
15
+
16
+ * Update the changelog with [GitHub Changelog Generator](https://github.com/skywinder/github-changelog-generator/) (`gem install github_changelog_generator` if you don't have it, set `CHANGELOG_GITHUB_TOKEN` to a personal access token to avoid rate limiting by GitHub). This command will update `CHANGELOG.md`. You need to commit and push manually.
17
+
18
+ github_changelog_generator
19
+
20
+ [twingly-rubygems]: https://rubygems.org/profiles/twingly
21
+ [ruby-prof]: http://ruby-prof.rubyforge.org/
22
+ [memory_profiler]: https://github.com/SamSaffron/memory_profiler
23
+ [examples]: examples/url.rb
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "logfmt_logger/middleware"
4
+
5
+ module Faraday
6
+ module LogfmtLogger
7
+ end
8
+ end
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009-2015 Rick Olson, Zack Hobson
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Faraday
6
+ module LogfmtLogger
7
+ class Middleware < Response::Middleware
8
+ extend Forwardable
9
+
10
+ DEFAULT_OPTIONS = { headers: true, bodies: false }.freeze
11
+
12
+ def initialize(app, logger = nil, options = {})
13
+ super(app)
14
+ @logger = logger || begin
15
+ require "logger"
16
+ ::Logger.new(STDOUT)
17
+ end
18
+
19
+ @filter = []
20
+ @options = DEFAULT_OPTIONS.merge(options)
21
+ yield self if block_given?
22
+ end
23
+
24
+ def_delegators :@logger, :debug, :info, :warn, :error, :fatal
25
+
26
+ def call(env)
27
+ info("request") do
28
+ log_entry = {
29
+ "source": "upstream-request",
30
+ "method": env.method.upcase,
31
+ "url": apply_filters(env.url.to_s),
32
+ "request_id": request_id,
33
+ }.merge(app_metadata)
34
+
35
+ Twingly::StringUtilities.logfmt(log_entry)
36
+ end
37
+ debug_log_request(env)
38
+ super
39
+ end
40
+
41
+ def on_complete(env)
42
+ info("response") do
43
+ log_entry = {
44
+ "source": "upstream-response",
45
+ "status": env.status,
46
+ "request_id": request_id,
47
+ }.merge(app_metadata)
48
+
49
+ Twingly::StringUtilities.logfmt(log_entry)
50
+ end
51
+ debug_log_response(env)
52
+ end
53
+
54
+ # Disable Rubocop here to keep the code look the same where it came from:
55
+ # https://github.com/lostisland/faraday/blob/v0.11.0/lib/faraday/response/logger.rb
56
+
57
+ # rubocop:disable all
58
+ def filter(filter_word, filter_replacement)
59
+ @filter.push([ filter_word, filter_replacement ])
60
+ end
61
+ # rubocop:enable all
62
+
63
+ private
64
+
65
+ def request_id
66
+ @options[:request_id]
67
+ end
68
+
69
+ def app_metadata
70
+ {
71
+ "release": Twingly::HTTP::Heroku.release_version,
72
+ }
73
+ end
74
+
75
+ # rubocop:disable all
76
+ def debug_log_request(env)
77
+ debug('request') { apply_filters( dump_headers env.request_headers ) } if log_headers?(:request)
78
+ debug('request') { apply_filters( dump_body(env[:body]) ) } if env[:body] && log_body?(:request)
79
+ end
80
+
81
+ def debug_log_response(env)
82
+ debug('response') { apply_filters( dump_headers env.response_headers ) } if log_headers?(:response)
83
+ debug('response') { apply_filters( dump_body env[:body] ) } if env[:body] && log_body?(:response)
84
+ end
85
+
86
+ def dump_headers(headers)
87
+ headers.map { |k, v| "#{k}: #{v.inspect}" }.join("\n")
88
+ end
89
+
90
+ def dump_body(body)
91
+ if body.respond_to?(:to_str)
92
+ body.to_str
93
+ else
94
+ pretty_inspect(body)
95
+ end
96
+ end
97
+
98
+ def pretty_inspect(body)
99
+ require 'pp' unless body.respond_to?(:pretty_inspect)
100
+ body.pretty_inspect
101
+ end
102
+
103
+ def log_headers?(type)
104
+ case @options[:headers]
105
+ when Hash then @options[:headers][type]
106
+ else @options[:headers]
107
+ end
108
+ end
109
+
110
+ def log_body?(type)
111
+ case @options[:bodies]
112
+ when Hash then @options[:bodies][type]
113
+ else @options[:bodies]
114
+ end
115
+ end
116
+
117
+ def apply_filters(output)
118
+ @filter.each do |pattern, replacement|
119
+ output = output.to_s.gsub(pattern, replacement)
120
+ end
121
+ output
122
+ end
123
+ # rubocop:enable all
124
+ end
125
+ end
126
+ end
127
+
128
+ Faraday::Response.register_middleware(
129
+ logfmt_logger: Faraday::LogfmtLogger::Middleware
130
+ )
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "url_size_limit/middleware"
4
+
5
+ module Faraday
6
+ module UrlSizeLimit
7
+ end
8
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Faraday
4
+ module UrlSizeLimit
5
+ class LimitExceededError < StandardError; end
6
+
7
+ class Middleware < Faraday::Middleware
8
+ def initialize(app, max_size_bytes:)
9
+ super(app)
10
+
11
+ @max_size_bytes = max_size_bytes
12
+ end
13
+
14
+ def call(env)
15
+ url_bytesize = env.url.to_s.bytesize
16
+
17
+ if url_bytesize >= @max_size_bytes
18
+ raise LimitExceededError,
19
+ "Expected URL below #{@max_size_bytes} bytes, "\
20
+ "was #{url_bytesize} bytes"
21
+ end
22
+
23
+ @app.call(env)
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ Faraday::Request.register_middleware(
30
+ url_size_limit: Faraday::UrlSizeLimit::Middleware
31
+ )
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twingly
4
+ module HTTP
5
+ class Heroku
6
+ def self.app_name
7
+ ENV.fetch("HEROKU_APP_NAME") { "unknown_heroku_app_name" }
8
+ end
9
+
10
+ def self.dyno_id
11
+ ENV.fetch("HEROKU_DYNO_ID") { "unknown_heroku_dyno_id" }
12
+ end
13
+
14
+ def self.slug_commit
15
+ ENV.fetch("HEROKU_SLUG_COMMIT") { "unknown_heroku_slug_commit" }
16
+ end
17
+
18
+ def self.release_version
19
+ ENV.fetch("HEROKU_RELEASE_VERSION") { "unknown_heroku_release_version" }
20
+ end
21
+
22
+ def self.review_app?
23
+ parent_name = ENV.fetch("HEROKU_PARENT_APP_NAME") {}
24
+
25
+ return false unless parent_name
26
+
27
+ app_name = ENV.fetch("HEROKU_APP_NAME") { "" }
28
+ review_app_name_format = /\A#{parent_name}-pr-\d+\z/
29
+
30
+ review_app_name_format.match?(app_name)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "faraday"
5
+
6
+ require_relative "../faraday/logfmt_logger"
7
+ require_relative "../faraday/url_size_limit"
8
+ require_relative "heroku"
9
+ require_relative "string_utilities"
10
+
11
+ module Twingly
12
+ module HTTP
13
+ class ConnectionError < StandardError; end
14
+ class UrlSizeLimitExceededError < StandardError; end
15
+ class Client # rubocop:disable Metrics/ClassLength
16
+ DEFAULT_RETRYABLE_EXCEPTIONS = [
17
+ Faraday::ConnectionFailed,
18
+ Faraday::SSLError,
19
+ Zlib::BufError,
20
+ Zlib::DataError,
21
+ ].freeze
22
+ TIMEOUT_EXCEPTIONS = [
23
+ Faraday::TimeoutError,
24
+ Net::OpenTimeout,
25
+ ].freeze
26
+ DEFAULT_HTTP_TIMEOUT = 20
27
+ DEFAULT_HTTP_OPEN_TIMEOUT = 10
28
+ DEFAULT_NUMBER_OF_RETRIES = 0
29
+ DEFAULT_RETRY_INTERVAL = 1
30
+ DEFAULT_MAX_URL_SIZE_BYTES = Float::INFINITY
31
+
32
+ attr_writer :http_timeout
33
+ attr_writer :http_open_timeout
34
+ attr_writer :number_of_retries
35
+ attr_writer :retry_interval
36
+ attr_writer :on_retry_callback
37
+ attr_writer :max_url_size_bytes
38
+ attr_writer :request_id
39
+
40
+ attr_accessor :retryable_exceptions
41
+
42
+ def initialize(logger:, base_user_agent:)
43
+ @logger = logger
44
+ @base_user_agent = base_user_agent
45
+ @request_id = nil
46
+
47
+ @http_timeout = DEFAULT_HTTP_TIMEOUT
48
+ @http_open_timeout = DEFAULT_HTTP_OPEN_TIMEOUT
49
+
50
+ @retryable_exceptions = DEFAULT_RETRYABLE_EXCEPTIONS
51
+ @number_of_retries = DEFAULT_NUMBER_OF_RETRIES
52
+ @retry_interval = DEFAULT_RETRY_INTERVAL
53
+ @on_retry_callback = nil
54
+
55
+ @max_url_size_bytes = DEFAULT_MAX_URL_SIZE_BYTES
56
+ end
57
+
58
+ def get(url, params: {})
59
+ http_response_for(:get, url: url, params: params)
60
+ end
61
+
62
+ def post(url, body:, headers: {})
63
+ http_response_for(:post, url: url, body: body, headers: headers)
64
+ end
65
+
66
+ private
67
+
68
+ # rubocop:disable Metrics/MethodLength
69
+ def http_response_for(method, *args)
70
+ response = case method
71
+ when :get
72
+ http_get_response(*args)
73
+ when :post
74
+ http_post_response(*args)
75
+ end
76
+
77
+ Response.new(headers: response.headers.to_h,
78
+ status: response.status,
79
+ body: response.body)
80
+ rescue *(@retryable_exceptions + TIMEOUT_EXCEPTIONS)
81
+ raise ConnectionError
82
+ rescue Faraday::UrlSizeLimit::LimitExceededError => error
83
+ raise UrlSizeLimitExceededError, error.message
84
+ end
85
+ # rubocop:enable all
86
+
87
+ def http_get_response(url:, params: {})
88
+ binary_url = url.dup.force_encoding(Encoding::BINARY)
89
+ http_client = create_http_client
90
+
91
+ headers = default_headers
92
+
93
+ http_client.get do |request|
94
+ request.url(binary_url)
95
+ request.params.merge!(params)
96
+ request.headers.merge!(headers)
97
+ request.options.timeout = @http_timeout
98
+ request.options.open_timeout = @http_open_timeout
99
+ end
100
+ end
101
+
102
+ def http_post_response(url:, body:, headers:)
103
+ binary_url = url.dup.force_encoding(Encoding::BINARY)
104
+ http_client = create_http_client
105
+
106
+ headers = default_headers.merge(headers)
107
+
108
+ http_client.post do |request|
109
+ request.url(binary_url)
110
+ request.headers.merge!(headers)
111
+ request.body = body
112
+ request.options.timeout = @http_timeout
113
+ request.options.open_timeout = @http_open_timeout
114
+ end
115
+ end
116
+
117
+ def create_http_client # rubocop:disable Metrics/MethodLength
118
+ Faraday.new do |faraday|
119
+ faraday.request :url_size_limit,
120
+ max_size_bytes: @max_url_size_bytes
121
+ faraday.request :retry,
122
+ max: @number_of_retries,
123
+ interval: @retry_interval,
124
+ exceptions: @retryable_exceptions,
125
+ methods: [], # empty [] forces Faraday to run retry_if
126
+ retry_if: retry_if
127
+ faraday.response :logfmt_logger, @logger.dup,
128
+ headers: true,
129
+ bodies: true,
130
+ request_id: @request_id
131
+ faraday.adapter Faraday.default_adapter
132
+ faraday.headers[:user_agent] = user_agent
133
+ end
134
+ end
135
+
136
+ def retry_if
137
+ lambda do |env, exception|
138
+ unwrapped_exception = unwrap_exception(exception)
139
+
140
+ # we do not retry on timeouts due to our request time budget
141
+ if timeout_error?(unwrapped_exception)
142
+ false
143
+ else
144
+ @on_retry_callback&.call(env, unwrapped_exception)
145
+ true
146
+ end
147
+ end
148
+ end
149
+
150
+ def unwrap_exception(exception)
151
+ if exception.respond_to?(:wrapped_exception)
152
+ exception.wrapped_exception
153
+ else
154
+ exception
155
+ end
156
+ end
157
+
158
+ def timeout_error?(error)
159
+ TIMEOUT_EXCEPTIONS.include?(error.class)
160
+ end
161
+
162
+ def user_agent
163
+ format(
164
+ "%<base>s (Release/%<release>s; Commit/%<commit>s)",
165
+ base: @base_user_agent,
166
+ release: Heroku.release_version,
167
+ commit: Heroku.slug_commit
168
+ )
169
+ end
170
+
171
+ def app_metadata
172
+ {
173
+ "dyno_id": Heroku.dyno_id,
174
+ "release": Heroku.release_version,
175
+ "git_head": Heroku.slug_commit,
176
+ }
177
+ end
178
+
179
+ def default_headers
180
+ {
181
+ "X-Request-Id": @request_id,
182
+ }.delete_if { |_name, value| value.to_s.strip.empty? }
183
+ end
184
+ end
185
+
186
+ class Response
187
+ attr_reader :headers
188
+ attr_reader :status
189
+ attr_reader :body
190
+
191
+ def initialize(headers: nil,
192
+ status: nil,
193
+ body: nil)
194
+ @headers = headers
195
+ @status = status
196
+ @body = body
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twingly
4
+ module StringUtilities
5
+ module_function
6
+
7
+ def logfmt(hsh)
8
+ hsh.map { |key, value| "#{key}=#{value}" }.join(" ")
9
+ end
10
+
11
+ def strip_start_character(string, char:)
12
+ return unless string
13
+
14
+ if string[0] == char
15
+ string[1..-1]
16
+ else
17
+ string
18
+ end
19
+ end
20
+
21
+ def split_key_pair(key_pair)
22
+ key_pair.to_s.split(":")
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twingly
4
+ module HTTP
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,180 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: twingly-http
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Twingly AB
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-11-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.15'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.15'
27
+ - !ruby/object:Gem::Dependency
28
+ name: climate_control
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.1'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '12'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '12'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.76'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.76'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop-rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.36'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.36'
97
+ - !ruby/object:Gem::Dependency
98
+ name: toxiproxy
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: vcr
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '5.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '5.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: webmock
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '3.7'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '3.7'
139
+ description: Robust HTTP client tailored by Twingly
140
+ email:
141
+ - support@twingly.com
142
+ executables: []
143
+ extensions: []
144
+ extra_rdoc_files: []
145
+ files:
146
+ - LICENSE
147
+ - README.md
148
+ - lib/faraday/logfmt_logger.rb
149
+ - lib/faraday/logfmt_logger/LICENSE.md
150
+ - lib/faraday/logfmt_logger/middleware.rb
151
+ - lib/faraday/url_size_limit.rb
152
+ - lib/faraday/url_size_limit/middleware.rb
153
+ - lib/twingly/heroku.rb
154
+ - lib/twingly/http.rb
155
+ - lib/twingly/string_utilities.rb
156
+ - lib/twingly/version.rb
157
+ homepage: http://github.com/twingly/twingly-http
158
+ licenses:
159
+ - MIT
160
+ metadata: {}
161
+ post_install_message:
162
+ rdoc_options: []
163
+ require_paths:
164
+ - lib
165
+ required_ruby_version: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - "~>"
168
+ - !ruby/object:Gem::Version
169
+ version: '2.5'
170
+ required_rubygems_version: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - ">="
173
+ - !ruby/object:Gem::Version
174
+ version: '0'
175
+ requirements: []
176
+ rubygems_version: 3.0.6
177
+ signing_key:
178
+ specification_version: 4
179
+ summary: Robust HTTP client
180
+ test_files: []