{"id":24932349,"url":"https://github.com/mediafinger/chimera_http_client","last_synced_at":"2025-10-27T10:33:58.706Z","repository":{"id":46571915,"uuid":"95446803","full_name":"mediafinger/chimera_http_client","owner":"mediafinger","description":"Unified way to access internal REST APIs or work with external JSON APIs for Ruby apps","archived":false,"fork":false,"pushed_at":"2025-05-22T10:42:33.000Z","size":112,"stargazers_count":3,"open_issues_count":0,"forks_count":2,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-10-09T06:09:23.457Z","etag":null,"topics":["api-client","gem","hacktoberfest","http-client","queue-request","ruby"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mediafinger.png","metadata":{"files":{"readme":"README.markdown","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-06-26T13:01:25.000Z","updated_at":"2025-05-22T10:42:31.000Z","dependencies_parsed_at":"2025-04-09T22:37:34.573Z","dependency_job_id":null,"html_url":"https://github.com/mediafinger/chimera_http_client","commit_stats":null,"previous_names":[],"tags_count":17,"template":false,"template_full_name":null,"purl":"pkg:github/mediafinger/chimera_http_client","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mediafinger%2Fchimera_http_client","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mediafinger%2Fchimera_http_client/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mediafinger%2Fchimera_http_client/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mediafinger%2Fchimera_http_client/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mediafinger","download_url":"https://codeload.github.com/mediafinger/chimera_http_client/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mediafinger%2Fchimera_http_client/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279275367,"owners_count":26138583,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-17T02:00:07.504Z","response_time":56,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api-client","gem","hacktoberfest","http-client","queue-request","ruby"],"created_at":"2025-02-02T14:21:14.676Z","updated_at":"2025-10-17T03:08:21.498Z","avatar_url":"https://github.com/mediafinger.png","language":"Ruby","readme":"# ChimeraHttpClient\n\nWhen starting to split monolithic apps into smaller services, you need an easy way to access the remote data from the other apps. This **chimera_http_client gem** should serve as **a comfortable and unifying way** to access endpoints from other apps.\n\nAnd what works for the internal communication between your own apps, will also work for external APIs that do not offer a client for simplified access.\n\nIt offers an **easy to learn interface** and **nice error handling**. And it enables you to **queue HTTP requests to run them in parallel** for better performance and simple aggregating of distributed data.\n\n[![GitHub Actions CI Build Status](https://github.com/mediafinger/chimera_http_client/actions/workflows/action-ci.yml/badge.svg?branch=master)](https://github.com/mediafinger/chimera_http_client/actions/workflows/action-ci.yml)\n[![Gem Version](https://badge.fury.io/rb/chimera_http_client.svg)](https://badge.fury.io/rb/chimera_http_client)\n\n## Dependencies\n\nThe `chimera_http_client` gem is wrapping the **libcurl** wrapper [**Typhoeus**](https://typhoeus.github.io/) to have a more convenient interface. This allows for fast requests, for caching responses, and for queueing requests to run them in parallel. Connections are persistent by default, which saves subsequent requests from establishing a connection.\n\nThe only other runtime dependency is Ruby's latest code loader [**zeitwerk**](https://github.com/fxn/zeitwerk) which is also part of Rails 6.\n^\n### Ruby version\n\n| Chimera version | MRI Ruby version                                    | JRuby | TruffleRuby |\n|:----------------|:----------------------------------------------------|:-----:|:-----------:|\n| \u003e= 1.7          | \u003e= 3.3 (older versions untested, likely still work) |  yes  |     yes     |\n| \u003e= 1.6          | \u003e= 2.7 (all 3.x versions supported)                 |  yes  |     yes     |\n| \u003e= 1.4          | \u003e= 2.5 (3.0 compatibility ensured)                  |  yes  |     no      |\n| \u003e= 1.1          | \u003e= 2.5                                              |   ?   |      ?      |\n| =  1.0          | \u003e= 2.4, \u003c= 3.0                                      |   ?   |      ?      |\n| \u003c= 0.5          | \u003e= 2.1, \u003c= 3.0                                      |   ?   |      ?      |\n\nThe test suite of v1.4 passes on **MRI Ruby** (2.5, 2.6, 2.7, 3.0) and on **JRuby**, but not on **TruffleRuby**.  \nThe test suite of v1.6 passes on **MRI Ruby** (2.7, 3.0, 3.1, 3.2, 3.3) and on **JRuby** and **TruffleRuby**.  \nThe non-MRI Rubys are not part of the regular Matrix, as their CI jobs take 3x as long, but included for new releases.\n\n### ENV variables\n\nSetting the environment variable `ENV['CHIMERA_HTTP_CLIENT_LOG_REQUESTS']` to `true` (or `'true'`) will provide more detailed error messages for logging and also add additional information to the Error JSON. It is recommended to use this only in development environments.\n\n## Table of Contents\n\n\u003c!-- TOC depthFrom:1 depthTo:4 withLinks:1 updateOnSave:0 orderedList:0 --\u003e\n\n* [ChimeraHttpClient](#chimerahttpclient)\n  * [Dependencies](#dependencies)\n    * [Ruby version](#ruby-version)\n    * [ENV variables](#env-variables)\n  * [Table of Contents](#table-of-contents)\n  * [The Connection class](#the-connection-class)\n    * [Initialization](#initialization)\n      * [Mandatory initialization parameter `base_url`](#mandatory-initialization-parameter-base_url)\n      * [Optional initialization parameters](#optional-initialization-parameters)\n        * [Custom deserializers](#custom-deserializers)\n        * [Monitoring, metrics, instrumentation](#monitoring-metrics-instrumentation)\n    * [Request methods](#request-methods)\n      * [Mandatory request parameter `endpoint`](#mandatory-request-parameter-endpoint)\n      * [Optional request parameters](#optional-request-parameters)\n      * [Basic auth](#basic-auth)\n      * [Timeout duration](#timeout-duration)\n      * [Custom logger](#custom-logger)\n      * [Caching responses](#caching-responses)\n    * [Example usage](#example-usage)\n  * [The Request class](#the-request-class)\n  * [The Response class](#the-response-class)\n  * [Error classes](#error-classes)\n  * [The Queue class](#the-queue-class)\n    * [Queueing requests](#queueing-requests)\n    * [Executing requests in parallel](#executing-requests-in-parallel)\n    * [Empty the queue](#empty-the-queue)\n  * [Installation](#installation)\n  * [Maintainers and Contributors](#maintainers-and-contributors)\n    * [Roadmap](#roadmap)\n  * [Chimera](#chimera)\n\n\u003c!-- /TOC --\u003e\n\n## The Connection class\n\nThe basic usage looks like this:\n\n```ruby\nconnection = ChimeraHttpClient::Connection.new(base_url: 'http://localhost/namespace')\nresponse = connection.get!(endpoint, params: params)\n```\n\n### Initialization\n\n`connection = ChimeraHttpClient::Connection.new(base_url: 'http://localhost:3000/v1', logger: logger, cache: cache)`\n\n#### Mandatory initialization parameter `base_url`\n\nThe mandatory parameter is **base_url** which should include the host, port and base path to the API endpoints you want to call, e.g. `'http://localhost:3000/v1'`.\n\nSetting the `base_url` is meant to be a comfort feature, as you can then pass short endpoints to each request like `/users`. You could set an empty string `''` as `base_url` and then pass full qualified URLs as endpoint of the requests.\n\n#### Optional initialization parameters\n\nThe optional parameters are:\n\n* `cache` - an instance of your cache solution, can be overwritten in any request\n* `deserializers` - custom methods to deserialize the response body, below more details\n* `logger` - an instance of a logger class that implements `#info`, `#warn` and `#error` methods\n* `monitor` - to collect metrics about requests, the basis for your instrumentation needs\n* `timeout` - the timeout for all requests, can be overwritten in any request, the default are 3 seconds\n* `user_agent` - if you would like your calls to identify with a specific user agent\n* `verbose` - the default is `false`, set it to true while debugging issues\n\n##### Custom deserializers\n\nIn case the API you are connecting to does not return JSON, you can pass custom deserializers to `Connection.new` or `Queue.new`:\n\n    deserializers: { error: your_error_deserializer, response: your_response_deserializer }\n\nA Deserializer has to be an object on which the method `call` with the parameter `body` can be called:\n\n    custom_deserializer.call(body)\n\nwhere `body` is the response body (in the default case a JSON object). The class `Deserializer` contains the default objects that are used. They might help you creating your own. Don't forget to make requests with another header than the default `\"Content-Type\" =\u003e \"application/json\"`, when the API you connect to does not support JSON.\n\n##### Monitoring, metrics, instrumentation\n\nPass an object as `:monitor` to a connection that defines the method `call` and accepts a hash as parameter.\n\n    monitor.call({...})\n\nIt will receive information about every request as soon as it finished. What you do with this information is up for you to implement.\n\n| Field          | Description                                                           |\n|:---------------|:----------------------------------------------------------------------|\n| `url`          | URL of the endpoint that was called                                   |\n| `method`       | HTTP method: get, post, ...                                           |\n| `status`       | HTTP status code: 200, ...                                            |\n| `runtime`      | the time in seconds it took the request to finish                     |\n| `completed_at` | Time.now.utc.iso8601(3)                                               |\n| `context`      | Whatever you pass as `monitoring_context` to the options of a request |\n\n### Request methods\n\nThe available methods are:\n\n* `get` / `get!`\n* `post` / `post!`\n* `put` / `put`\n* `patch` / `patch!`\n* `delete` / `delete!`\n\nwhere the methods ending on a _bang!_ will raise an error (which you should handle in your application) while the others will return an error object.\n\n#### Mandatory request parameter `endpoint`\n\nThe `base_url` set in the connection will together with the `endpoint` determine the URL to make a request to.\n\n```ruby\nconnection.get([:users, id])\nconnection.get([\"users\", id])\nconnection.get(\"users/#{id}\")\nconnection.get(\"/users/#{id}\")\n```\n\nAll forms above ave valid and will make a request to the same URL.\n\n* Please take note that _the endpoint can be given as a String, a Symbol, or an Array._\n* While they do no harm, there is _no need to pass leading or trailing `/` in endpoints._\n* When passing the endpoint as an Array, _it's elements are converted to Strings and concatenated with `/`._\n\n#### Optional request parameters\n\nAll request methods expect a mandatory `endpoint` and an optional hash as parameters. In the latter the following keywords are treated specially:\n\n* `body` - the mandatory body of a `post`, `put` or `patch` request\n* `headers` - a hash of HTTP headers\n* `params` - parameters of a HTTP request\n* `username` - used for a BasicAuth login\n* `password` - used for a BasicAuth login\n* `timeout` - set a custom timeout per request (the default is 3 seconds)\n* `cache` - optionally overwrite the cache store set in `Connection` in any request\n* `monitoring_context` - pass additional information you want to collect with your instrumentation `monitor`\n\nExample:\n\n```ruby\nconnection.post(\n  :users,\n  body: { name: \"Andy\" },\n  params: { origin: `Twitter`},\n  headers: { \"Authorization\" =\u003e \"Bearer #{token}\" },\n  timeout: 10,\n  cache: nil\n)\n```\n\n#### Basic auth\n\nIn case you need to use an API that is protected by **basic_auth** just pass the credentials as optional parameters:\n`username: 'admin', password: 'secret'`\n\n#### Timeout duration\n\nThe default timeout duration is **3 seconds**.\n\nIf you want to use a different timeout, you can pass the key `timeout` when initializing the `Connection`. You can also overwrite it on every call.\n\n#### Custom logger\n\nBy default no logging is happening. If you need request logging, you can pass your custom Logger to the key `logger` when initializing the `Connection`. It will write to `logger.info` when starting and when completing a request.\n\nThe message passed to the logger is a hash with the following fields:\n\n| Key          | Description                                 |\n|:-------------|:--------------------------------------------|\n| `message`    | indicator if a call was started or finished |\n| `method`     | the HTTP method used                        |\n| `url`        | the requested URL                           |\n| `code`       | HTTP status code                            |\n| `runtime`    | time the request took in ms                 |\n| `user_agent` | the user_agent used to open the connection  |\n\n#### Caching responses\n\nTo cache all the reponses of a connection, just pass the optional parameter `cache` to its initializer. You can also overwrite the connection's cache configuration by passing the parameter `cache` to any `get` call.\n\nIt could be an instance of an implementation as simple as this:\n\n```ruby\nclass Cache\n  def initialize\n    @memory = {}\n  end\n\n  def get(request)\n    @memory[request]\n  end\n\n  def set(request, response)\n    @memory[request] = response\n  end\nend\n```\n\nOr use an adapter for Dalli, Redis, or Rails cache that also support an optional time-to-live `default_ttl` parameter. If you use `Rails.cache` with the adapter `:memory_store` or `:mem_cache_store`, the object you would have to pass looks like this:\n\n```ruby\nrequire \"typhoeus/cache/rails\"\n\ncache: Typhoeus::Cache::Rails.new(Rails.cache, default_ttl: 600) # 600 seconds\n```\n\nRead more about how to use it: https://github.com/typhoeus/typhoeus#caching\n\n### Example usage\n\nTo use the gem, it is recommended to write wrapper classes for the endpoints used. While it would be possible to use the `get, get!, post, post!, put, put!, patch, patch!, delete, delete!` or also the bare `request.run` methods directly, wrapper classes will unify the usage pattern and be very convenient to use by veterans and newcomers to the team. A wrapper class could look like this:\n\n```ruby\nrequire 'chimera_http_client'\n\nclass Users\n  def initialize(base_url: 'http://localhost:3000/v1')\n    @base_url = base_url\n  end\n\n  # GET one user by id and instantiate a User\n  #\n  def find(id:)\n    response = connection.get!(['users', id])\n\n    user = response.parsed_body\n    User.new(id: id, name: user['name'], email: user['email'])\n\n  rescue ChimeraHttpClient::Error =\u003e error\n    # handle / log / raise error\n  end\n\n  # GET a list of users and instantiate an Array of Users\n  #\n  def all(filter: nil, page: nil)\n    params = {}\n    params[:filter] = filter\n    params[:page] = page\n\n    response = connection.get!('users', params: params, timeout: 10) # set longer timeout\n\n    all_users = response.parsed_body\n    all_users.map { |user| User.new(id: user['id'], name: user['name'], email: user['email']) }\n\n  rescue ChimeraHttpClient::Error =\u003e error\n    # handle / log / raise error\n  end\n\n  # CREATE a new user by sending attributes in a JSON body and instantiate the new User\n  #\n  def create(body:)\n    response = connection.post!('users', body: body.to_json) # body.to_json (!!)\n\n    user = response.parsed_body\n    User.new(id: user['id'], name: user['name'], email: user['email'])\n\n  rescue ChimeraHttpClient::Error =\u003e error\n    # handle / log / raise error\n  end\n\n  private\n\n  def connection\n    # base_url is mandatory\n    # logger and timeout are optional\n    @connection ||= ChimeraHttpClient::Connection.new(base_url: @base_url, logger: Logger.new(STDOUT), timeout: 2)\n  end\nend\n```\n\nTo create and fetch a user from a remote service with the `Users` wrapper listed above, calls could be made like this:\n\n```ruby\n  users = Users.new\n\n  new_user = users.create(body: { name: \"Andy\", email: \"andy@example.com\" })\n  id = new_user.id\n\n  user = users.find(id: id)\n  user.name # == \"Andy\"\n```\n\n## The Request class\n\nUsually it does not have to be used directly. It is the class that executes the `Typhoeus::Requests`, raises `Errors` on failing and returns `Response` objects on successful calls.\n\nThe `body` which it receives from the `Connection` class has to be in the in the (serialized) form in which the endpoint expects it. Usually this means you have to pass a JSON string to the `body` (it will **not** be serialized automatically).\n\n## The Response class\n\nThe `ChimeraHttpClient::Response` objects have the following interface:\n\n    * body             (content the call returns)\n    * code             (http code, should be 200 or 2xx)\n    * time             (for monitoring)\n    * response         (the full response object, including the request)\n    * success?         (returns the result of response.success?)\n    * error?           (returns false)\n    * parsed_body      (returns the result of `deserializer[:response].call(body)`)\n\nIf your API does not use JSON, but a different format e.g. XML, you can pass a custom deserializer to the Connection.\n\n## Error classes\n\nAll errors inherit from `ChimeraHttpClient::Error` and therefore offer the same attributes:\n\n    * code             (http error code)\n    * body             (alias =\u003e message)\n    * time             (for monitoring)\n    * response         (the full response object, including the request)\n    * success?         (returns the result of response.success?)\n    * error?           (returns true)\n    * error_class      (e.g. ChimeraHttpClient::NotFoundError)\n    * to_s             (information for logging / respects ENV['CHIMERA_HTTP_CLIENT_LOG_REQUESTS'])\n    * to_json          (information to return to the API consumer / respects ENV['CHIMERA_HTTP_CLIENT_LOG_REQUESTS'])\n\nThe error classes and their corresponding http error codes:\n\n    ConnectionError           # 0\n    RedirectionError          # 301, 302, 303, 307\n    BadRequestError           # 400\n    UnauthorizedError         # 401\n    PaymentRequiredError      # 402\n    ForbiddenError            # 403\n    NotFoundError             # 404\n    MethodNotAllowedError     # 405\n    ResourceConflictError     # 409\n    UnprocessableEntityError  # 422\n    ClientError               # 400..499\n    ServerError               # 500..599\n    TimeoutError              # timeout\n\n## The Queue class\n\nInstead of making single requests immediately, the ChimeraHttpClient allows to queue requests and run them in **parallel**.\n\nThe number of parallel requests is limited by your system. There is a hard limit for 200 concurrent requests. You will have to measure yourself where the sweet spot for optimal performance is - and when things start to get flaky. I recommend to queue not much more than 20 requests before running them.\n\n### Queueing requests\n\nThe initializer of the `Queue` class expects and handles the same parameters as the `Connection` class.\n\n```ruby\nqueue = ChimeraHttpClient::Queue.new(base_url: 'http://localhost:3000/v1')\n```\n\n`queue.add` expects and handles the same parameters as the requests methods of a connection.\n\n```ruby\nqueue.add(method, endpoint, options = {})\n```\n\nThe only difference is that a parameter to set the HTTP method has to prepended. Valid options for `method` are:\n\n* `:get` / `'get'` / `'GET'`\n* `:post` / `'post'` / `'POST'`\n* `:put` / `'put'` / `'PUT'`\n* `:patch` / `'patch'` / `'PATCH'`\n* `:delete` / `'delete'` / `'DELETE'`\n\n### Executing requests in parallel\n\nOnce the queue is filled, run all the requests concurrently with:\n\n```ruby\nresponses = queue.execute\n```\n\n`responses` will contain an Array of `ChimeraHttpClient::Response` objects when all calls succeed. If any calls fail, the Array will also contain `ChimeraHttpClient::Error` objects. It is in your responsibility to handle the errors.\n\n\u003e Tip: every `Response` and every `Error` make the underlying `Typheous::Request` available over `object.response.request`, which could help with debugging, or with building your own retry mechanism.\n\n### Empty the queue\n\nThe queue is emptied after execution. You could also empty it at any other point before by calling `queue.empty`.\n\nTo inspect the requests waiting for execution, call `queue.queued_requests`.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n    gem 'chimera_http_client', '~\u003e 1.1'\n\nAnd then execute:\n\n    $ bundle\n\nWhen updating the version, do not forget to run\n\n    $ bundle update chimera_http_client\n\n## Maintainers and Contributors\n\nAfter checking out the repo, run `bundle install` and then `bundle execute rake` to run the **tests and rubocop**.\n\n\u003e The test suite uses a Sinatra server to make real HTTP requests. It is mounted via Capybara_discoball and running in the same process. It is still running reasonably fast (on my MacBook Air):\n\n    Finished in 2.01 seconds (files took 1.09 seconds to load)\n    824 examples, 0 failures, 7 pending\n\nYou can also run `rake console` to open an irb session with the `ChimeraHttpClient` pre-loaded that will allow you to experiment.\n\nTo build and install this gem onto your local machine, run `bundle exec rake install`.\n\n\u003e Maintainers only:\n\u003e\n\u003e To release a new version, update the version number in `version.rb`, commit this change, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).\n\nBug reports and pull requests are welcome on GitHub at \u003chttps://github.com/mediafinger/chimera_http_client\u003e\n\n### Roadmap\n\nhttps://github.com/mediafinger/chimera_http_client/blob/master/TODO.markdown\n\n## Chimera\n\nWhy this name? First of all, I needed a unique namespace. _HttpClient_ is already used too often. And as this gem is based on **Typhoeus** I picked the name of one of his (mythological) children.\n\n\u003chttps://en.wikipedia.org/wiki/Chimera_(mythology)\u003e\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmediafinger%2Fchimera_http_client","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmediafinger%2Fchimera_http_client","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmediafinger%2Fchimera_http_client/lists"}