dhc 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/rubocop.yml +27 -0
  3. data/.github/workflows/test.yml +27 -0
  4. data/.gitignore +37 -0
  5. data/.rubocop.yml +105 -0
  6. data/.ruby-version +1 -0
  7. data/Gemfile +5 -0
  8. data/LICENSE +674 -0
  9. data/README.md +999 -0
  10. data/Rakefile +25 -0
  11. data/dhc.gemspec +40 -0
  12. data/lib/core_ext/hash/deep_transform_values.rb +48 -0
  13. data/lib/dhc.rb +77 -0
  14. data/lib/dhc/concerns/dhc/basic_methods_concern.rb +42 -0
  15. data/lib/dhc/concerns/dhc/configuration_concern.rb +20 -0
  16. data/lib/dhc/concerns/dhc/fix_invalid_encoding_concern.rb +42 -0
  17. data/lib/dhc/concerns/dhc/formats_concern.rb +25 -0
  18. data/lib/dhc/concerns/dhc/request/user_agent_concern.rb +25 -0
  19. data/lib/dhc/config.rb +47 -0
  20. data/lib/dhc/endpoint.rb +119 -0
  21. data/lib/dhc/error.rb +82 -0
  22. data/lib/dhc/errors/client_error.rb +73 -0
  23. data/lib/dhc/errors/parser_error.rb +4 -0
  24. data/lib/dhc/errors/server_error.rb +28 -0
  25. data/lib/dhc/errors/timeout.rb +4 -0
  26. data/lib/dhc/errors/unknown_error.rb +4 -0
  27. data/lib/dhc/format.rb +18 -0
  28. data/lib/dhc/formats.rb +8 -0
  29. data/lib/dhc/formats/form.rb +45 -0
  30. data/lib/dhc/formats/json.rb +55 -0
  31. data/lib/dhc/formats/multipart.rb +45 -0
  32. data/lib/dhc/formats/plain.rb +42 -0
  33. data/lib/dhc/interceptor.rb +36 -0
  34. data/lib/dhc/interceptors.rb +26 -0
  35. data/lib/dhc/interceptors/auth.rb +94 -0
  36. data/lib/dhc/interceptors/caching.rb +148 -0
  37. data/lib/dhc/interceptors/default_timeout.rb +16 -0
  38. data/lib/dhc/interceptors/logging.rb +37 -0
  39. data/lib/dhc/interceptors/monitoring.rb +92 -0
  40. data/lib/dhc/interceptors/prometheus.rb +51 -0
  41. data/lib/dhc/interceptors/retry.rb +41 -0
  42. data/lib/dhc/interceptors/rollbar.rb +36 -0
  43. data/lib/dhc/interceptors/throttle.rb +86 -0
  44. data/lib/dhc/interceptors/zipkin.rb +110 -0
  45. data/lib/dhc/railtie.rb +9 -0
  46. data/lib/dhc/request.rb +161 -0
  47. data/lib/dhc/response.rb +60 -0
  48. data/lib/dhc/response/data.rb +28 -0
  49. data/lib/dhc/response/data/base.rb +18 -0
  50. data/lib/dhc/response/data/collection.rb +16 -0
  51. data/lib/dhc/response/data/item.rb +29 -0
  52. data/lib/dhc/rspec.rb +11 -0
  53. data/lib/dhc/test/cache_helper.rb +3 -0
  54. data/lib/dhc/version.rb +5 -0
  55. data/script/ci/build.sh +19 -0
  56. data/spec/basic_methods/delete_spec.rb +34 -0
  57. data/spec/basic_methods/get_spec.rb +49 -0
  58. data/spec/basic_methods/post_spec.rb +42 -0
  59. data/spec/basic_methods/put_spec.rb +48 -0
  60. data/spec/basic_methods/request_spec.rb +19 -0
  61. data/spec/basic_methods/request_without_rails_spec.rb +29 -0
  62. data/spec/config/endpoints_spec.rb +63 -0
  63. data/spec/config/placeholders_spec.rb +32 -0
  64. data/spec/core_ext/hash/deep_transform_values_spec.rb +24 -0
  65. data/spec/dummy/README.rdoc +28 -0
  66. data/spec/dummy/Rakefile +8 -0
  67. data/spec/dummy/app/assets/config/manifest.js +3 -0
  68. data/spec/dummy/app/assets/images/.keep +0 -0
  69. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  70. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  71. data/spec/dummy/app/controllers/application_controller.rb +7 -0
  72. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  73. data/spec/dummy/app/helpers/application_helper.rb +4 -0
  74. data/spec/dummy/app/mailers/.keep +0 -0
  75. data/spec/dummy/app/models/.keep +0 -0
  76. data/spec/dummy/app/models/concerns/.keep +0 -0
  77. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  78. data/spec/dummy/bin/bundle +5 -0
  79. data/spec/dummy/bin/rails +6 -0
  80. data/spec/dummy/bin/rake +6 -0
  81. data/spec/dummy/config.ru +6 -0
  82. data/spec/dummy/config/application.rb +16 -0
  83. data/spec/dummy/config/boot.rb +7 -0
  84. data/spec/dummy/config/environment.rb +7 -0
  85. data/spec/dummy/config/environments/development.rb +36 -0
  86. data/spec/dummy/config/environments/production.rb +77 -0
  87. data/spec/dummy/config/environments/test.rb +41 -0
  88. data/spec/dummy/config/initializers/assets.rb +10 -0
  89. data/spec/dummy/config/initializers/backtrace_silencers.rb +9 -0
  90. data/spec/dummy/config/initializers/cookies_serializer.rb +5 -0
  91. data/spec/dummy/config/initializers/filter_parameter_logging.rb +6 -0
  92. data/spec/dummy/config/initializers/inflections.rb +18 -0
  93. data/spec/dummy/config/initializers/mime_types.rb +6 -0
  94. data/spec/dummy/config/initializers/session_store.rb +5 -0
  95. data/spec/dummy/config/initializers/wrap_parameters.rb +11 -0
  96. data/spec/dummy/config/locales/en.yml +23 -0
  97. data/spec/dummy/config/routes.rb +58 -0
  98. data/spec/dummy/config/secrets.yml +22 -0
  99. data/spec/dummy/lib/assets/.keep +0 -0
  100. data/spec/dummy/log/.keep +0 -0
  101. data/spec/dummy/public/404.html +67 -0
  102. data/spec/dummy/public/422.html +67 -0
  103. data/spec/dummy/public/500.html +66 -0
  104. data/spec/dummy/public/favicon.ico +0 -0
  105. data/spec/endpoint/compile_spec.rb +35 -0
  106. data/spec/endpoint/match_spec.rb +41 -0
  107. data/spec/endpoint/placeholders_spec.rb +30 -0
  108. data/spec/endpoint/remove_interpolated_params_spec.rb +17 -0
  109. data/spec/endpoint/values_as_params_spec.rb +31 -0
  110. data/spec/error/dup_spec.rb +12 -0
  111. data/spec/error/find_spec.rb +57 -0
  112. data/spec/error/response_spec.rb +17 -0
  113. data/spec/error/timeout_spec.rb +14 -0
  114. data/spec/error/to_s_spec.rb +85 -0
  115. data/spec/formats/form_spec.rb +27 -0
  116. data/spec/formats/json_spec.rb +66 -0
  117. data/spec/formats/multipart_spec.rb +26 -0
  118. data/spec/formats/plain_spec.rb +29 -0
  119. data/spec/interceptors/after_request_spec.rb +20 -0
  120. data/spec/interceptors/after_response_spec.rb +39 -0
  121. data/spec/interceptors/auth/basic_auth_spec.rb +17 -0
  122. data/spec/interceptors/auth/bearer_spec.rb +19 -0
  123. data/spec/interceptors/auth/body_spec.rb +36 -0
  124. data/spec/interceptors/auth/long_basic_auth_credentials_spec.rb +17 -0
  125. data/spec/interceptors/auth/no_instance_var_for_options_spec.rb +27 -0
  126. data/spec/interceptors/auth/reauthentication_configuration_spec.rb +61 -0
  127. data/spec/interceptors/auth/reauthentication_spec.rb +44 -0
  128. data/spec/interceptors/before_request_spec.rb +21 -0
  129. data/spec/interceptors/before_response_spec.rb +20 -0
  130. data/spec/interceptors/caching/hydra_spec.rb +26 -0
  131. data/spec/interceptors/caching/main_spec.rb +73 -0
  132. data/spec/interceptors/caching/methods_spec.rb +42 -0
  133. data/spec/interceptors/caching/multilevel_cache_spec.rb +139 -0
  134. data/spec/interceptors/caching/options_spec.rb +78 -0
  135. data/spec/interceptors/caching/parameters_spec.rb +24 -0
  136. data/spec/interceptors/caching/response_status_spec.rb +29 -0
  137. data/spec/interceptors/caching/to_cache_spec.rb +16 -0
  138. data/spec/interceptors/default_interceptors_spec.rb +15 -0
  139. data/spec/interceptors/default_timeout/main_spec.rb +34 -0
  140. data/spec/interceptors/define_spec.rb +30 -0
  141. data/spec/interceptors/dup_spec.rb +19 -0
  142. data/spec/interceptors/logging/main_spec.rb +37 -0
  143. data/spec/interceptors/monitoring/caching_spec.rb +66 -0
  144. data/spec/interceptors/monitoring/main_spec.rb +97 -0
  145. data/spec/interceptors/prometheus_spec.rb +54 -0
  146. data/spec/interceptors/response_competition_spec.rb +39 -0
  147. data/spec/interceptors/retry/main_spec.rb +73 -0
  148. data/spec/interceptors/return_response_spec.rb +38 -0
  149. data/spec/interceptors/rollbar/invalid_encoding_spec.rb +43 -0
  150. data/spec/interceptors/rollbar/main_spec.rb +57 -0
  151. data/spec/interceptors/throttle/main_spec.rb +236 -0
  152. data/spec/interceptors/throttle/reset_track_spec.rb +53 -0
  153. data/spec/interceptors/zipkin/distributed_tracing_spec.rb +135 -0
  154. data/spec/rails_helper.rb +6 -0
  155. data/spec/request/body_spec.rb +39 -0
  156. data/spec/request/encoding_spec.rb +38 -0
  157. data/spec/request/error_handling_spec.rb +88 -0
  158. data/spec/request/headers_spec.rb +12 -0
  159. data/spec/request/ignore_errors_spec.rb +73 -0
  160. data/spec/request/option_dup_spec.rb +13 -0
  161. data/spec/request/parallel_requests_spec.rb +59 -0
  162. data/spec/request/params_encoding_spec.rb +26 -0
  163. data/spec/request/request_without_rails_spec.rb +15 -0
  164. data/spec/request/url_patterns_spec.rb +54 -0
  165. data/spec/request/user_agent_spec.rb +26 -0
  166. data/spec/request/user_agent_without_rails_spec.rb +27 -0
  167. data/spec/response/body_spec.rb +16 -0
  168. data/spec/response/code_spec.rb +16 -0
  169. data/spec/response/data_accessor_spec.rb +29 -0
  170. data/spec/response/data_spec.rb +85 -0
  171. data/spec/response/effective_url_spec.rb +16 -0
  172. data/spec/response/headers_spec.rb +18 -0
  173. data/spec/response/options_spec.rb +18 -0
  174. data/spec/response/success_spec.rb +13 -0
  175. data/spec/response/time_spec.rb +21 -0
  176. data/spec/spec_helper.rb +9 -0
  177. data/spec/support/fixtures/json/feedback.json +11 -0
  178. data/spec/support/fixtures/json/feedbacks.json +164 -0
  179. data/spec/support/fixtures/json/localina_content_ad.json +23 -0
  180. data/spec/support/load_json.rb +5 -0
  181. data/spec/support/reset_config.rb +8 -0
  182. data/spec/support/zipkin_mock.rb +114 -0
  183. data/spec/timeouts/no_signal_spec.rb +13 -0
  184. data/spec/timeouts/timings_spec.rb +55 -0
  185. metadata +527 -0
data/README.md ADDED
@@ -0,0 +1,999 @@
1
+ DHC is an advanced HTTP client.
2
+ Implementing basic http-communication enhancements like interceptors, exception handling, format handling, accessing response data, configuring endpoints and placeholders and fully compatible, RFC-compliant URL-template support.
3
+
4
+ DHC uses [typhoeus](https://github.com/typhoeus/typhoeus) for low level http communication.
5
+
6
+ See [DHS](https://github.com/DePayFi/DHS), if you are searching for something more **high level** that can query webservices easily and provides an ActiveRecord like interface.
7
+
8
+ ## Quick start guide
9
+
10
+ ```ruby
11
+ gem install dhc
12
+ ```
13
+
14
+ or add it to your Gemfile:
15
+
16
+ ```ruby
17
+ gem 'dhc'
18
+ ```
19
+
20
+ use it like:
21
+
22
+ ```ruby
23
+ response = DHC.get('http://datastore/v2/feedbacks')
24
+ response.data.items[0]
25
+ response.data.items[0].recommended
26
+ response.body
27
+ response.headers
28
+ ```
29
+
30
+ ## Basic methods
31
+
32
+ Available are `get`, `post`, `put` & `delete`.
33
+
34
+ Other methods are available using `DHC.request(options)`.
35
+
36
+ ## Request
37
+
38
+ The request class handles the http request, implements the interceptor pattern, loads configured endpoints, generates urls from url-templates and raises [exceptions](#exceptions) for any response code that is not indicating success (2xx).
39
+
40
+ ```ruby
41
+ response = DHC.request(url: 'http://depay.fi', method: :options)
42
+
43
+ response.request.response #<DHC::Response> the associated response.
44
+
45
+ response.request.options #<Hash> the options used for creating the request.
46
+
47
+ response.request.params # access request params
48
+
49
+ response.request.headers # access request headers
50
+
51
+ response.request.url #<String> URL that is used for doing the request
52
+
53
+ response.request.method #<Symbol> provides the used http-method
54
+ ```
55
+
56
+ ### Formats
57
+
58
+ You can use any of the basic methods in combination with a format like `json`:
59
+
60
+ ```ruby
61
+ DHC.json.get(options)
62
+ ```
63
+
64
+ Currently supported formats: `json`, `multipart`, `plain` (for no formatting)
65
+
66
+ If formats are used, headers for `Content-Type` and `Accept` are enforced by DHC, but also http bodies are translated by DHC, so you can pass bodies as ruby objects:
67
+
68
+ ```ruby
69
+ DHC.json.post('http://slack', body: { text: 'Hi there' })
70
+ # Content-Type: application/json
71
+ # Accept: application/json
72
+ # Translates body to "{\"text\":\"Hi there\"}" before sending
73
+ ```
74
+
75
+ #### Default format
76
+
77
+ If you use DHC's basic methods `DHC.get`, `DHC.post` etc. without any explicit format, `JSON` will be chosen as the default format.
78
+
79
+ #### Unformatted requests
80
+
81
+ In case you need to send requests without DHC formatting headers or the body, use `plain`:
82
+
83
+ ```ruby
84
+ DHC.plain.post('http://endpoint', body: { weird: 'format%s2xX' })
85
+ ```
86
+
87
+ ##### Upload with DHC
88
+
89
+ If you want to upload data with DHC, it's recommended to use the `multipart` format:
90
+
91
+ ```ruby
92
+ response = DHC.multipart.post('http://upload', body: { file })
93
+ response.headers['Location']
94
+ # Content-Type: multipart/form-data
95
+ # Leaves body unformatted
96
+ ```
97
+
98
+ ### Parallel requests
99
+
100
+ If you pass an array of requests to `DHC.request`, it will perform those requests in parallel.
101
+ You will get back an array of DHC::Response objects in the same order of the passed requests.
102
+
103
+ ```ruby
104
+ options = []
105
+ options << { url: 'http://datastore/v2/feedbacks' }
106
+ options << { url: 'http://datastore/v2/content-ads/123/feedbacks' }
107
+ responses = DHC.request(options)
108
+ ```
109
+
110
+ ```ruby
111
+ DHC.request([request1, request2, request3])
112
+ # returns [response1, response2, response3]
113
+ ```
114
+
115
+ ### Follow redirects
116
+
117
+ ```ruby
118
+ DHC.get('http://depay.fi', followlocation: true)
119
+ ```
120
+
121
+ ### Transfer data through the request body
122
+
123
+ Data that is transfered using the HTTP request body is transfered using the selected format, or the default `json`, so you need to provide it as a ruby object.
124
+
125
+ Also consider setting the http header for content-type or use one of the provided [formats](#formats), like `DHC.json`.
126
+
127
+ ```ruby
128
+ DHC.post('http://datastore/v2/feedbacks',
129
+ body: feedback,
130
+ headers: { 'Content-Type' => 'application/json' }
131
+ )
132
+ ```
133
+
134
+ ### Request parameters
135
+
136
+ When using DHC, try to pass params via `params` option. It's not recommended to build a url and attach the parameters yourself:
137
+
138
+ DO
139
+ ```ruby
140
+ DHC.get('http://depay.fi', params: { q: 'Restaurant' })
141
+ ```
142
+
143
+ DON'T
144
+ ```ruby
145
+ DHC.get('http://depay.fi?q=Restaurant')
146
+ ```
147
+
148
+ #### Array Parameter Encoding
149
+
150
+ DHC can encode array parameters in URLs in two ways. The default is `:rack` which generates URL parameters compatible with Rack and Rails.
151
+
152
+ ```ruby
153
+ DHC.get('http://depay.fi', params: { q: [1, 2] })
154
+ # http://depay.fi?q[]=1&q[]=2
155
+ ```
156
+
157
+ Some Java-based apps expect their arrays in the `:multi` format:
158
+
159
+ ```ruby
160
+ DHC.get('http://depay.fi', params: { q: [1, 2] }, params_encoding: :multi)
161
+ # http://depay.fi?q=1&q=2
162
+ ```
163
+
164
+ ### Request URL encoding
165
+
166
+ DHC, by default, encodes urls:
167
+
168
+ ```ruby
169
+ DHC.get('http://depay.fi?q=some space')
170
+ # http://depay.fi?q=some%20space
171
+
172
+ DHC.get('http://depay.fi', params: { q: 'some space' })
173
+ # http://depay.fi?q=some%20space
174
+ ```
175
+
176
+ which can be disabled:
177
+
178
+ ```ruby
179
+ DHC.get('http://depay.fi?q=some space', url_encoding: false)
180
+ # http://depay.fi?q=some space
181
+ ```
182
+
183
+ ### Request URL-Templates
184
+
185
+ Instead of using concrete urls you can also use url-templates that contain placeholders.
186
+ This is especially handy for configuring an endpoint once and generate the url from the params when doing the request.
187
+ Since version `7.0` url templates follow the [RFC 6750](https://tools.ietf.org/html/rfc6570).
188
+
189
+ ```ruby
190
+ DHC.get('http://datastore/v2/feedbacks/{id}', params:{ id: 123 })
191
+ # GET http://datastore/v2/feedbacks/123
192
+ ```
193
+
194
+ You can also use URL templates, when [configuring endpoints](#configuring-endpoints):
195
+ ```ruby
196
+ DHC.configure do |c|
197
+ c.endpoint(:find_feedback, 'http://datastore/v2/feedbacks/{id}')
198
+ end
199
+
200
+ DHC.get(:find_feedback, params:{ id: 123 }) # GET http://datastore/v2/feedbacks/123
201
+ ```
202
+
203
+ If you miss to provide a parameter that is part of the url-template, it will raise an exception.
204
+
205
+ ### Request timeout
206
+
207
+ Working and configuring timeouts is important, to ensure your app stays alive when services you depend on start to get really slow...
208
+
209
+ DHC forwards two timeout options directly to typhoeus:
210
+
211
+ `timeout` (in seconds) - The maximum time in seconds that you allow the libcurl transfer operation to take. Normally, name lookups can take a considerable time and limiting operations to less than a few seconds risk aborting perfectly normal operations. This option may cause libcurl to use the SIGALRM signal to timeout system calls.
212
+ `connecttimeout` (in seconds) - It should contain the maximum time in seconds that you allow the connection phase to the server to take. This only limits the connection phase, it has no impact once it has connected. Set to zero to switch to the default built-in connection timeout - 300 seconds.
213
+
214
+ ```ruby
215
+ DHC.get('http://depay.fi', timeout: 5, connecttimeout: 1)
216
+ ```
217
+
218
+ DHC provides a [timeout interceptor](#default-timeout-interceptor) that lets you apply default timeout values to all the requests that you are performig in your application.
219
+
220
+ ### Request Agent
221
+
222
+ DHC identifies itself towards outher services, using the `User-Agent` header.
223
+
224
+ ```
225
+ User-Agent DHC (9.4.2) [https://github.com/DePayFi/dhc]
226
+ ```
227
+
228
+ If DHC is used in an Rails Application context, also the application name is added to the `User-Agent` header.
229
+
230
+ ```
231
+ User-Agent DHC (9.4.2; MyRailsApplicationName) [https://github.com/DePayFi/dhc]
232
+ ```
233
+
234
+ ## Response
235
+
236
+ ```ruby
237
+ response.request #<DHC::Request> the associated request.
238
+
239
+ response.data #<OpenStruct> in case response body contains parsable JSON.
240
+ response.data.something.nested
241
+
242
+ response.body #<String>
243
+
244
+ response.code #<Fixnum>
245
+
246
+ response.headers #<Hash>
247
+
248
+ response.time #<Fixnum> Provides response time in ms.
249
+
250
+ response.timeout? #true|false
251
+ ```
252
+
253
+ ### Accessing response data
254
+
255
+ The response data can be access with dot-notation and square-bracket notation. You can convert response data to open structs or json (if the response format is json).
256
+
257
+ ```ruby
258
+ response = DHC.request(url: 'http://datastore/entry/1')
259
+ response.data.as_open_struct #<OpenStruct name='depay.fi'>
260
+ response.data.as_json # { name: 'depay.fi' }
261
+ response.data.name # 'depay.fi'
262
+ response.data[:name] # 'depay.fi'
263
+ ```
264
+
265
+ You can also access response data directly through the response object (with square bracket notation only):
266
+
267
+ ```ruby
268
+ DHC.json.get(url: 'http://datastore/entry/1')[:name]
269
+ ```
270
+
271
+ ## Exceptions
272
+
273
+ Anything but a response code indicating success (2xx) raises an exception.
274
+
275
+ ```ruby
276
+
277
+ DHC.get('localhost') # UnknownError: 0
278
+ DHC.get('http://localhost:3000') # DHC::Timeout: 0
279
+
280
+ ```
281
+
282
+ You can access the response object that was causing the error.
283
+
284
+ ```ruby
285
+ DHC.get('depay.fi')
286
+ rescue => e
287
+ e.response #<DHC:Response>
288
+ e.response.code # 403
289
+ e.response.timeout? # false
290
+ Rails.logger.error e
291
+ # DHC::UnknownError: get http://depay.fi
292
+ # Params: {:url=>"http://depay.fi", :method=>:get}
293
+ # Response Code: 0
294
+ # <Response Body>
295
+ ```
296
+
297
+ All errors that are raise by DHC inherit from `DHC::Error`.
298
+ They are divided into `DHC::ClientError`, `DHC::ServerError`, `DHC::Timeout` and `DHC::UnkownError` and mapped according to the following status code.
299
+
300
+ ```ruby
301
+ 400 => DHC::BadRequest
302
+ 401 => DHC::Unauthorized
303
+ 402 => DHC::PaymentRequired
304
+ 403 => DHC::Forbidden
305
+ 403 => DHC::Forbidden
306
+ 404 => DHC::NotFound
307
+ 405 => DHC::MethodNotAllowed
308
+ 406 => DHC::NotAcceptable
309
+ 407 => DHC::ProxyAuthenticationRequired
310
+ 408 => DHC::RequestTimeout
311
+ 409 => DHC::Conflict
312
+ 410 => DHC::Gone
313
+ 411 => DHC::LengthRequired
314
+ 412 => DHC::PreconditionFailed
315
+ 413 => DHC::RequestEntityTooLarge
316
+ 414 => DHC::RequestUriToLong
317
+ 415 => DHC::UnsupportedMediaType
318
+ 416 => DHC::RequestedRangeNotSatisfiable
319
+ 417 => DHC::ExpectationFailed
320
+ 422 => DHC::UnprocessableEntity
321
+ 423 => DHC::Locked
322
+ 424 => DHC::FailedDependency
323
+ 426 => DHC::UpgradeRequired
324
+
325
+ 500 => DHC::InternalServerError
326
+ 501 => DHC::NotImplemented
327
+ 502 => DHC::BadGateway
328
+ 503 => DHC::ServiceUnavailable
329
+ 504 => DHC::GatewayTimeout
330
+ 505 => DHC::HttpVersionNotSupported
331
+ 507 => DHC::InsufficientStorage
332
+ 510 => DHC::NotExtended
333
+
334
+ timeout? => DHC::Timeout
335
+
336
+ anything_else => DHC::UnknownError
337
+ ```
338
+
339
+ ### Custom error handling (rescue)
340
+
341
+ You can provide custom error handlers to handle errors happening during the request.
342
+
343
+ If a error handler is provided nothing is raised.
344
+
345
+ If your error handler returns anything else but `nil` it replaces the response body.
346
+
347
+ ```ruby
348
+ handler = ->(response){ do_something_with_response; return {name: 'unknown'} }
349
+ response = DHC.get('http://something', rescue: handler)
350
+ response.data.name # 'unknown'
351
+ ```
352
+
353
+ ### Ignore certain errors
354
+
355
+ As it's discouraged to rescue errors and then don't handle them (ruby styleguide)[https://github.com/bbatsov/ruby-style-guide#dont-hide-exceptions],
356
+ but you often want to continue working with `nil`, DHC provides the `ignore` option.
357
+
358
+ Errors listed in this option will not be raised and will leave the `response.body` and `response.data` to stay `nil`.
359
+
360
+ You can either pass the DHC error class you want to be ignored or an array of DHC error classes.
361
+
362
+ ```ruby
363
+ response = DHC.get('http://something', ignore: DHC::NotFound)
364
+
365
+ response.body # nil
366
+ response.data # nil
367
+ response.error_ignored? # true
368
+ response.request.error_ignored? # true
369
+ ```
370
+
371
+ ## Configuration
372
+
373
+ If you want to configure DHC, do it on initialization (like in a Rails initializer, `environment.rb` or `application.rb`), otherwise you could run into the problem that certain configurations can only be set once.
374
+
375
+ You can use `DHC.configure` to prevent the initialization problem.
376
+ Take care that you only use `DHC.configure` once, because it is actually reseting previously made configurations and applies the new once.
377
+
378
+ ```ruby
379
+
380
+ DHC.configure do |c|
381
+ c.placeholder :datastore, 'http://datastore'
382
+ c.endpoint :feedbacks, '{+datastore}/feedbacks', params: { has_reviews: true }
383
+ c.interceptors = [CachingInterceptor, MonitorInterceptor, TrackingIdInterceptor]
384
+ end
385
+
386
+ ```
387
+
388
+ ### Configuring endpoints
389
+
390
+ You can configure endpoints, for later use, by giving them a name, a url and some parameters (optional).
391
+
392
+ ```ruby
393
+ DHC.configure do |c|
394
+ c.endpoint(:feedbacks, 'http://datastore/v2/feedbacks', params: { has_reviews: true })
395
+ c.endpoint(:find_feedback, 'http://datastore/v2/feedbacks/{id}')
396
+ end
397
+
398
+ DHC.get(:feedbacks) # GET http://datastore/v2/feedbacks
399
+ DHC.get(:find_feedback, params:{ id: 123 }) # GET http://datastore/v2/feedbacks/123
400
+ ```
401
+
402
+ Explicit request options override configured options.
403
+
404
+ ```ruby
405
+ DHC.get(:feedbacks, params: { has_reviews: false }) # Overrides configured params
406
+ ```
407
+
408
+ ### Configuring placeholders
409
+
410
+ You can configure global placeholders, that are used when generating urls from url-templates.
411
+
412
+ ```ruby
413
+ DHC.configure do |c|
414
+ c.placeholder(:datastore, 'http://datastore')
415
+ c.endpoint(:feedbacks, '{+datastore}/feedbacks', { params: { has_reviews: true } })
416
+ end
417
+
418
+ DHC.get(:feedbacks) # http://datastore/v2/feedbacks
419
+ ```
420
+
421
+ ## Interceptors
422
+
423
+ To monitor and manipulate the HTTP communication done with DHC, you can define interceptors that follow the (Inteceptor Pattern)[https://en.wikipedia.org/wiki/Interceptor_pattern].
424
+ There are some interceptors that are part of DHC already, so called [Core Interceptors](#core-interceptors), that cover some basic usecases.
425
+
426
+ ### Quick start: Configure/Enable Interceptors
427
+
428
+ ```ruby
429
+ DHC.configure do |c|
430
+ c.interceptors = [DHC::Auth, DHC::Caching, DHC::DefaultTimeout, DHC::Logging, DHC::Monitoring, DHC::Prometheus, DHC::Retry, DHC::Rollbar, DHC::Zipkin]
431
+ end
432
+ ```
433
+
434
+ You can only set the list of global interceptors once and you can not alter it after you set it.
435
+
436
+ ### Interceptors on local request level
437
+
438
+ You can override the global list of interceptors on local request level:
439
+
440
+ ```ruby
441
+ interceptors = DHC.config.interceptors
442
+ interceptors -= [DHC::Caching] # remove caching
443
+ interceptors += [DHC::Retry] # add retry
444
+ DHC.request({url: 'http://depay.fi', retry: 2, interceptors: interceptors})
445
+
446
+ DHC.request({url: 'http://depay.fi', interceptors: []}) # no interceptor for this request at all
447
+ ```
448
+
449
+ ### Core Interceptors
450
+
451
+ #### Authentication Interceptor
452
+
453
+ Add the auth interceptor to your basic set of DHC interceptors.
454
+
455
+ ```ruby
456
+ DHC.configure do |c|
457
+ c.interceptors = [DHC::Auth]
458
+ end
459
+ ```
460
+
461
+ ##### Bearer Authentication
462
+
463
+ ```ruby
464
+ DHC.get('http://depay.fi', auth: { bearer: -> { access_token } })
465
+ ```
466
+
467
+ Adds the following header to the request:
468
+ ```
469
+ 'Authorization': 'Bearer 123456'
470
+ ```
471
+
472
+ Assuming the method `access_token` responds on runtime of the request with `123456`.
473
+
474
+ ##### Basic Authentication
475
+
476
+ ```ruby
477
+ DHC.get('http://depay.fi', auth: { basic: { username: 'steve', password: 'can' } })
478
+ ```
479
+
480
+ Adds the following header to the request:
481
+ ```
482
+ 'Authorization': 'Basic c3RldmU6Y2Fu'
483
+ ```
484
+
485
+ Which is the base64 encoded credentials "username:password".
486
+
487
+ ##### Body Authentication
488
+
489
+ ```ruby
490
+ DHC.post('http://depay.fi', auth: { body: { userToken: 'dheur5hrk3' } })
491
+ ```
492
+
493
+ Adds the following to body of all requests:
494
+
495
+ ```
496
+ {
497
+ "userToken": "dheur5hrk3"
498
+ }
499
+ ```
500
+
501
+ ##### Reauthenticate
502
+
503
+ The current implementation can only offer reauthenticate for _client access tokens_. For this to work the following has to be given:
504
+
505
+ * You have configured and implemented `DHC::Auth.refresh_client_token = -> { TokenRefreshUtil.client_access_token(true) }` which when called will force a refresh of the token and return the new value. It is also expected that this implementation will handle invalidating caches if necessary.
506
+ * Your interceptors contain `DHC::Auth` and `DHC::Retry`, whereas `DHC::Retry` comes _after_ `DHC::Auth` in the chain.
507
+
508
+ ##### Bearer Authentication with client access token
509
+
510
+ Reauthentication will be initiated if:
511
+
512
+ * setup is correct
513
+ * `response.success?` is false and an `DHC::Unauthorized` was observed
514
+ * reauthentication wasn't already attempted once
515
+
516
+ If this is the case, this happens:
517
+
518
+ * refresh the client token, by calling `refresh_client_token`
519
+ * the authentication header will be updated with the new token
520
+ * `DHC::Retry` will be triggered by adding `retry: { max: 1 }` to the request options
521
+
522
+ #### Caching Interceptor
523
+
524
+ Add the cache interceptor to your basic set of DHC interceptors.
525
+
526
+ ```ruby
527
+ DHC.configure do |c|
528
+ c.interceptors = [DHC::Caching]
529
+ end
530
+ ```
531
+
532
+ You can configure your own cache (default Rails.cache) and logger (default Rails.logger):
533
+
534
+ ```ruby
535
+ DHC::Caching.cache = ActiveSupport::Cache::MemoryStore.new
536
+ ```
537
+
538
+ Caching is not enabled by default, although you added it to your basic set of interceptors.
539
+ If you want to have requests served/stored and stored in/from cache, you have to enable it by request.
540
+
541
+ ```ruby
542
+ DHC.get('http://depay.fi', cache: true)
543
+ ```
544
+
545
+ You can also enable caching when configuring an endpoint in LHS.
546
+
547
+ ```ruby
548
+ class Feedbacks < LHS::Service
549
+ endpoint '{+datastore}/v2/feedbacks', cache: true
550
+ end
551
+ ```
552
+
553
+ Only GET requests are cached by default. If you want to cache any other request method, just configure it:
554
+
555
+ ```ruby
556
+ DHC.get('http://depay.fi', cache: { methods: [:get] })
557
+ ```
558
+
559
+ Responses served from cache are marked as served from cache:
560
+
561
+ ```ruby
562
+ response = DHC.get('http://depay.fi', cache: true)
563
+ response.from_cache? # true
564
+ ```
565
+
566
+ You can also use a central http cache to be used by the `DHC::Caching` interceptor.
567
+
568
+ If you configure a local and a central cache, DHC will perform multi-level-caching.
569
+ DHC will try to retrieve cached information first from the central, in case of a miss from the local cache, while writing back into both.
570
+
571
+ ```ruby
572
+ DHC::Caching.central = {
573
+ read: 'redis://$PASSWORD@central-http-cache-replica.namespace:6379/0',
574
+ write: 'redis://$PASSWORD@central-http-cache-master.namespace:6379/0'
575
+ }
576
+ ```
577
+
578
+ ##### Options
579
+
580
+ ```ruby
581
+ DHC.get('http://depay.fi', cache: { key: 'key' expires_in: 1.day, race_condition_ttl: 15.seconds, use: ActiveSupport::Cache::MemoryStore.new })
582
+ ```
583
+
584
+ `expires_in` - lets the cache expires every X seconds.
585
+
586
+ `key` - Set the key that is used for caching by using the option. Every key is prefixed with `DHC_CACHE(v1): `.
587
+
588
+ `race_condition_ttl` - very useful in situations where a cache entry is used very frequently and is under heavy load.
589
+ If a cache expires and due to heavy load several different processes will try to read data natively and then they all will try to write to cache.
590
+ To avoid that case the first process to find an expired cache entry will bump the cache expiration time by the value set in `race_condition_ttl`.
591
+
592
+ `use` - Set an explicit cache to be used for this request. If this option is missing `DHC::Caching.cache` is used.
593
+
594
+ #### Default Timeout Interceptor
595
+
596
+ Applies default timeout values to all requests made in an application, that uses DHC.
597
+
598
+ ```ruby
599
+ DHC.configure do |c|
600
+ c.interceptors = [DHC::DefaultTimeout]
601
+ end
602
+ ```
603
+
604
+ `timeout` default: 15 seconds
605
+ `connecttimeout` default: 2 seconds
606
+
607
+ ##### Overwrite defaults
608
+
609
+ ```ruby
610
+ DHC::DefaultTimeout.timeout = 5 # seconds
611
+ DHC::DefaultTimeout.connecttimeout = 3 # seconds
612
+ ```
613
+
614
+ #### Logging Interceptor
615
+
616
+ The logging interceptor logs all requests done with DHC to the Rails logs.
617
+
618
+ ##### Installation
619
+
620
+ ```ruby
621
+ DHC.configure do |c|
622
+ c.interceptors = [DHC::Logging]
623
+ end
624
+
625
+ DHC::Logging.logger = Rails.logger
626
+ ```
627
+
628
+ ##### What and how it logs
629
+
630
+ The logging Interceptor logs basic information about the request and the response:
631
+
632
+ ```ruby
633
+ DHC.get('http://depay.fi')
634
+ # Before DHC request<70128730317500> GET http://depay.fi at 2018-05-23T07:53:19+02:00 Params={} Headers={\"User-Agent\"=>\"Typhoeus - https://github.com/typhoeus/typhoeus\", \"Expect\"=>\"\"}
635
+ # After DHC response for request<70128730317500>: GET http://depay.fi at 2018-05-23T07:53:28+02:00 Time=0ms URL=http://depay.fi:80/
636
+ ```
637
+
638
+ ##### Configure
639
+
640
+ You can configure the logger beeing used by the logging interceptor:
641
+
642
+ ```ruby
643
+ DHC::Logging.logger = Another::Logger
644
+ ```
645
+
646
+ #### Monitoring Interceptor
647
+
648
+ The monitoring interceptor reports all requests done with DHC to a given StatsD instance.
649
+
650
+ ##### Installation
651
+
652
+ ```ruby
653
+ DHC.configure do |c|
654
+ c.interceptors = [DHC::Monitoring]
655
+ end
656
+ ```
657
+
658
+ You also have to configure statsd in order to have the monitoring interceptor report.
659
+
660
+ ```ruby
661
+ DHC::Monitoring.statsd = <your-instance-of-statsd>
662
+ ```
663
+
664
+ ##### Environment
665
+
666
+ By default, the monitoring interceptor uses Rails.env to determine the environment. In case you want to configure that, use:
667
+
668
+ ```ruby
669
+ DHC::Monitoring.env = ENV['DEPLOYMENT_TYPE'] || Rails.env
670
+ ```
671
+
672
+ ##### What it tracks
673
+
674
+ It tracks request attempts with `before_request` and `after_request` (counts).
675
+
676
+ In case your workers/processes are getting killed due limited time constraints,
677
+ you are able to detect deltas with relying on "before_request", and "after_request" counts:
678
+
679
+ ###### Before and after request tracking
680
+
681
+ ```ruby
682
+ "dhc.<app_name>.<env>.<host>.<http_method>.before_request", 1
683
+ "dhc.<app_name>.<env>.<host>.<http_method>.after_request", 1
684
+ ```
685
+
686
+ ###### Response tracking
687
+
688
+ In case of a successful response it reports the response code with a count and the response time with a gauge value.
689
+
690
+ ```ruby
691
+ DHC.get('http://depay.fi')
692
+
693
+ "dhc.<app_name>.<env>.<host>.<http_method>.count", 1
694
+ "dhc.<app_name>.<env>.<host>.<http_method>.200", 1
695
+ "dhc.<app_name>.<env>.<host>.<http_method>.time", 43
696
+ ```
697
+
698
+ In case of a unsuccessful response it reports the response code with a count but no time:
699
+
700
+ ```ruby
701
+ DHC.get('http://depay.fi')
702
+
703
+ "dhc.<app_name>.<env>.<host>.<http_method>.count", 1
704
+ "dhc.<app_name>.<env>.<host>.<http_method>.500", 1
705
+ ```
706
+
707
+ ###### Timeout tracking
708
+
709
+ Timeouts are also reported:
710
+
711
+ ```ruby
712
+ "dhc.<app_name>.<env>.<host>.<http_method>.timeout", 1
713
+ ```
714
+
715
+ All the dots in the host are getting replaced with underscore, because dot is the default separator in graphite.
716
+
717
+ ###### Caching tracking
718
+
719
+ When you want to track caching stats please make sure you have enabled the `DHC::Caching` and the `DHC::Monitoring` interceptor.
720
+
721
+ Make sure that the `DHC::Caching` is listed before `DHC::Monitoring` interceptor when configuring interceptors:
722
+
723
+ ```ruby
724
+ DHC.configure do |c|
725
+ c.interceptors = [DHC::Caching, DHC::Monitoring]
726
+ end
727
+ ```
728
+
729
+ If a response was served from cache it tracks:
730
+
731
+ ```ruby
732
+ "dhc.<app_name>.<env>.<host>.<http_method>.cache.hit", 1
733
+ ```
734
+
735
+ If a response was not served from cache it tracks:
736
+
737
+ ```ruby
738
+ "dhc.<app_name>.<env>.<host>.<http_method>.cache.miss", 1
739
+ ```
740
+
741
+ ##### Configure
742
+
743
+ It is possible to set the key for Monitoring Interceptor on per request basis:
744
+
745
+ ```ruby
746
+ DHC.get('http://depay.fi', monitoring_key: 'local_website')
747
+
748
+ "local_website.count", 1
749
+ "local_website.200", 1
750
+ "local_website.time", 43
751
+ ```
752
+
753
+ If you use this approach you need to add all namespaces (app, environment etc.) to the key on your own.
754
+
755
+
756
+ #### Prometheus Interceptor
757
+
758
+ Logs basic request/response information to prometheus.
759
+
760
+ ```ruby
761
+ require 'prometheus/client'
762
+
763
+ DHC.configure do |c|
764
+ c.interceptors = [DHC::Prometheus]
765
+ end
766
+
767
+ DHC::Prometheus.client = Prometheus::Client
768
+ DHC::Prometheus.namespace = 'web_location_app'
769
+ ```
770
+
771
+ ```ruby
772
+ DHC.get('http://depay.fi')
773
+ ```
774
+
775
+ - Creates a prometheus counter that receives additional meta information for: `:code`, `:success` and `:timeout`.
776
+
777
+ - Creates a prometheus histogram for response times in milliseconds.
778
+
779
+
780
+ #### Retry Interceptor
781
+
782
+ If you enable the retry interceptor, you can have DHC retry requests for you:
783
+
784
+ ```ruby
785
+ DHC.configure do |c|
786
+ c.interceptors = [DHC::Retry]
787
+ end
788
+
789
+ response = DHC.get('http://depay.fi', retry: true)
790
+ ```
791
+
792
+ It will try to retry the request up to 3 times (default) internally, before it passes the last response back, or raises an error for the last response.
793
+
794
+ Consider, that all other interceptors will run for every single retry.
795
+
796
+ ##### Limit the amount of retries while making the request
797
+
798
+ ```ruby
799
+ DHC.get('http://depay.fi', retry: { max: 1 })
800
+ ```
801
+
802
+ ##### Change the default maximum of retries of the retry interceptor
803
+
804
+ ```ruby
805
+ DHC::Retry.max = 3
806
+ ```
807
+
808
+ ##### Retry all requests
809
+
810
+ If you want to retry all requests made from your application, you just need to configure it globally:
811
+
812
+ ```ruby
813
+ DHC::Retry.all = true
814
+ configuration.interceptors = [DHC::Retry]
815
+ ```
816
+
817
+ ##### Do not retry certain response codes
818
+
819
+ If you do not want to retry based on certain response codes, use retry in combination with explicit `ignore`:
820
+
821
+ ```ruby
822
+ DHC.get('http://depay.fi', ignore: DHC::NotFound, retry: { max: 1 })
823
+ ```
824
+
825
+ Or if you use `DHC::Retry.all`:
826
+
827
+ ```ruby
828
+ DHC.get('http://depay.fi', ignore: DHC::NotFound)
829
+ ```
830
+
831
+ #### Rollbar Interceptor
832
+
833
+ Forward errors to rollbar when exceptions occur during http requests.
834
+
835
+ ```ruby
836
+ DHC.configure do |c|
837
+ c.interceptors = [DHC::Rollbar]
838
+ end
839
+ ```
840
+
841
+ ```ruby
842
+ DHC.get('http://depay.fi')
843
+ ```
844
+
845
+ If it raises, it forwards the request and response object to rollbar, which contain all necessary data.
846
+
847
+ ##### Forward additional parameters
848
+
849
+ ```ruby
850
+ DHC.get('http://depay.fi', rollbar: { tracking_key: 'this particular request' })
851
+ ```
852
+
853
+ #### Throttle
854
+
855
+ The throttle interceptor allows you to raise an exception if a predefined quota of a provider request limit is reached in advance.
856
+
857
+ ```ruby
858
+ DHC.configure do |c|
859
+ c.interceptors = [DHC::Throttle]
860
+ end
861
+ ```
862
+ ```ruby
863
+ options = {
864
+ throttle: {
865
+ track: true,
866
+ break: '80%',
867
+ provider: 'depay.fi',
868
+ limit: { header: 'Rate-Limit-Limit' },
869
+ remaining: { header: 'Rate-Limit-Remaining' },
870
+ expires: { header: 'Rate-Limit-Reset' }
871
+ }
872
+ }
873
+
874
+ DHC.get('http://depay.fi', options)
875
+ # { headers: { 'Rate-Limit-Limit' => 100, 'Rate-Limit-Remaining' => 19 } }
876
+
877
+ DHC.get('http://depay.fi', options)
878
+ # raises DHC::Throttle::OutOfQuota: Reached predefined quota for depay.fi
879
+ ```
880
+
881
+ **Options Description**
882
+ * `track`: enables tracking of current limit/remaining requests of rate-limiting
883
+ * `break`: quota in percent after which errors are raised. Percentage symbol is optional, values will be converted to integer (e.g. '23.5' will become 23)
884
+ * `provider`: name of the provider under which throttling tracking is aggregated,
885
+ * `limit`:
886
+ * a hard-coded integer
887
+ * a hash pointing at the response header containing the limit value
888
+ * a proc that receives the response as argument and returns the limit value
889
+ * `remaining`:
890
+ * a hash pointing at the response header containing the current amount of remaining requests
891
+ * a proc that receives the response as argument and returns the current amount of remaining requests
892
+ * `expires`:
893
+ * a hash pointing at the response header containing the timestamp when the quota will reset
894
+ * a proc that receives the response as argument and returns the timestamp when the quota will reset
895
+
896
+
897
+ #### Zipkin
898
+
899
+ ** Zipkin 0.33 breaks our current implementation of the Zipkin interceptor **
900
+
901
+ Zipkin is a distributed tracing system. It helps gather timing data needed to troubleshoot latency problems in microservice architectures [Zipkin Distributed Tracing](https://zipkin.io/).
902
+
903
+ Add the zipkin interceptor to your basic set of DHC interceptors.
904
+
905
+ ```ruby
906
+ DHC.configure do |c|
907
+ c.interceptors = [DHC::Zipkin]
908
+ end
909
+ ```
910
+
911
+ The following configuration needs to happen in the application that wants to run this interceptor:
912
+
913
+ 1. Add `gem 'zipkin-tracer', '< 0.33.0'` to your Gemfile.
914
+ 2. Add the necessary Rack middleware and configuration
915
+
916
+ ```ruby
917
+ config.middleware.use ZipkinTracer::RackHandler, {
918
+ service_name: 'service-name', # name your service will be known as in zipkin
919
+ service_port: 80, # the port information that is sent along the trace
920
+ json_api_host: 'http://zipkin-collector', # the zipkin endpoint
921
+ sample_rate: 1 # sample rate, where 1 = 100% of all requests, and 0.1 is 10% of all requests
922
+ }
923
+ ```
924
+
925
+ ### Create an interceptor from scratch
926
+
927
+ ```ruby
928
+ class TrackingIdInterceptor < DHC::Interceptor
929
+
930
+ def before_request
931
+ request.params[:tid] = 123
932
+ end
933
+ end
934
+ ```
935
+
936
+ ```ruby
937
+ DHC.configure do |c|
938
+ c.interceptors = [TrackingIdInterceptor]
939
+ end
940
+ ```
941
+
942
+ #### Interceptor callbacks
943
+
944
+ `before_raw_request` is called before the raw typhoeus request is prepared/created.
945
+
946
+ `before_request` is called when the request is prepared and about to be executed.
947
+
948
+ `after_request` is called after request was started.
949
+
950
+ `before_response` is called when response started to arrive.
951
+
952
+ `after_response` is called after the response arrived completely.
953
+
954
+
955
+ #### Interceptor request/response
956
+
957
+ Every interceptor can directly access their instance [request](#request) or [response](#response).
958
+
959
+ #### Provide a response replacement through an interceptor
960
+
961
+ Inside an interceptor, you are able to provide a response, rather then doing a real request.
962
+ This is useful for implementing e.g. caching.
963
+
964
+ ```ruby
965
+ class DHC::Cache < DHC::Interceptor
966
+
967
+ def before_request(request)
968
+ cached_response = Rails.cache.fetch(request.url)
969
+ return DHC::Response.new(cached_response) if cached_response
970
+ end
971
+ end
972
+ ```
973
+
974
+ Take care that having more than one interceptor trying to return a response will cause an exception.
975
+ You can access the request.response to identify if a response was already provided by another interceptor.
976
+
977
+ ```ruby
978
+ class RemoteCacheInterceptor < DHC::Interceptor
979
+
980
+ def before_request(request)
981
+ return unless request.response.nil?
982
+ return DHC::Response.new(remote_cache)
983
+ end
984
+ end
985
+ ```
986
+
987
+ ## Testing
988
+
989
+ When writing tests for your application when using DHC, please make sure you require the dhc rspec test helper:
990
+
991
+ ```ruby
992
+ # spec/spec_helper.rb
993
+
994
+ require 'dhc/rspec'
995
+ ```
996
+
997
+ ## License
998
+
999
+ [GNU General Public License Version 3.](https://www.gnu.org/licenses/gpl-3.0.en.html)