{"id":13483405,"url":"https://github.com/icyleaf/halite","last_synced_at":"2025-08-20T10:32:00.169Z","repository":{"id":46247376,"uuid":"101950421","full_name":"icyleaf/halite","owner":"icyleaf","description":"💎HTTP Requests Client with a chainable REST API, built-in sessions and middlewares.","archived":false,"fork":false,"pushed_at":"2022-03-22T02:42:32.000Z","size":6495,"stargazers_count":171,"open_issues_count":2,"forks_count":13,"subscribers_count":6,"default_branch":"master","last_synced_at":"2024-12-10T00:42:03.099Z","etag":null,"topics":["chainable-methods","crystal","http-client","http-interceptor","http-logger","http-middleware","http-session"],"latest_commit_sha":null,"homepage":"","language":"Crystal","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/icyleaf.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-08-31T02:32:20.000Z","updated_at":"2024-11-27T04:18:03.000Z","dependencies_parsed_at":"2022-08-31T02:40:53.700Z","dependency_job_id":null,"html_url":"https://github.com/icyleaf/halite","commit_stats":null,"previous_names":[],"tags_count":33,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/icyleaf%2Fhalite","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/icyleaf%2Fhalite/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/icyleaf%2Fhalite/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/icyleaf%2Fhalite/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/icyleaf","download_url":"https://codeload.github.com/icyleaf/halite/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":230415318,"owners_count":18222158,"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","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":["chainable-methods","crystal","http-client","http-interceptor","http-logger","http-middleware","http-session"],"created_at":"2024-07-31T17:01:10.867Z","updated_at":"2024-12-19T10:09:24.415Z","avatar_url":"https://github.com/icyleaf.png","language":"Crystal","readme":"![halite-logo](https://github.com/icyleaf/halite/raw/master/halite-logo-small.png)\n\n# Halite\n\n[![Language](https://img.shields.io/badge/language-crystal-776791.svg)](https://github.com/crystal-lang/crystal)\n[![Tag](https://img.shields.io/github/tag/icyleaf/halite.svg)](https://github.com/icyleaf/halite/blob/master/CHANGELOG.md)\n[![Source](https://img.shields.io/badge/source-github-brightgreen.svg)](https://github.com/icyleaf/halite/)\n[![Document](https://img.shields.io/badge/document-api-brightgreen.svg)](https://icyleaf.github.io/halite/)\n[![Build Status](https://github.com/icyleaf/halite/workflows/Linux%20CI/badge.svg)](https://github.com/icyleaf/halite/actions?query=workflow%3A%22Linux+CI%22)\n\nHTTP Requests with a chainable REST API, built-in sessions and middleware written by [Crystal](https://crystal-lang.org/).\nInspired from the **awesome** Ruby's [HTTP](https://github.com/httprb/http)/[RESTClient](https://github.com/rest-client/rest-client)\nand Python's [requests](https://github.com/requests/requests).\n\nBuild in Crystal version `\u003e= 1.0.0`, this document valid with latest commit.\n\n## Index\n\n\u003c!-- TOC --\u003e\n\n- [Installation](#installation)\n- [Usage](#usage)\n  - [Making Requests](#making-requests)\n  - [Passing Parameters](#passing-parameters)\n    - [Query string parameters](#query-string-parameters)\n    - [Form data](#form-data)\n    - [File uploads (via form data)](#file-uploads-via-form-data)\n    - [JSON data](#json-data)\n    - [Raw String](#raw-string)\n  - [Passing advanced options](#passing-advanced-options)\n    - [Auth](#auth)\n    - [User Agent](#user-agent)\n    - [Headers](#headers)\n    - [Cookies](#cookies)\n    - [Redirects and History](#redirects-and-history)\n    - [Timeout](#timeout)\n  - [HTTPS](#https)\n  - [Response Handling](#response-handling)\n    - [Response Content](#response-content)\n    - [JSON Content](#json-content)\n    - [Parsing Content](#parsing-content)\n    - [Binary Data](#binary-data)\n  - [Error Handling](#error-handling)\n    - [Raise for status code](#raise-for-status-code)\n- [Middleware](#middleware)\n  - [Write a simple feature](#write-a-simple-feature)\n  - [Write a interceptor](#write-a-interceptor)\n- [Advanced Usage](#advanced-usage)\n  - [Configuring](#configuring)\n  - [Endpoint](#endpoint)\n  - [Sessions](#sessions)\n  - [Streaming Requests](#streaming-requests)\n  - [Logging](#logging)\n  - [Local Cache](#local-cache)\n  - [Link Headers](#link-headers)\n\u003c!-- /TOC --\u003e\n\n## Installation\n\nAdd this to your application's `shard.yml`:\n\n```yaml\ndependencies:\n  halite:\n    github: icyleaf/halite\n```\n\n## Usage\n\n```crystal\nrequire \"halite\"\n```\n\n### Making Requests\n\nMake a GET request:\n\n```crystal\n# Direct get url\nHalite.get(\"http://httpbin.org/get\")\n\n# Support NamedTuple as query params\nHalite.get(\"http://httpbin.org/get\", params: {\n  language: \"crystal\",\n  shard: \"halite\"\n})\n\n# Also support Hash as query params\nHalite.get(\"http://httpbin.org/get\", headers: {\n    \"Private-Token\" =\u003e \"T0k3n\"\n  }, params: {\n    \"language\" =\u003e \"crystal\",\n    \"shard\" =\u003e \"halite\"\n  })\n\n# And support chainable\nHalite.header(private_token: \"T0k3n\")\n      .get(\"http://httpbin.org/get\", params: {\n        \"language\" =\u003e \"crystal\",\n        \"shard\" =\u003e \"halite\"\n      })\n```\n\nSee also all [chainable methods](https://icyleaf.github.io/halite/Halite/Chainable.html).\n\nMany other HTTP methods are available as well:\n\n- `get`\n- `head`\n- `post`\n- `put`\n- `delete`\n- `patch`\n- `options`\n\n### Passing Parameters\n\n#### Query string parameters\n\nUse the `params` argument to add query string parameters to requests:\n\n```crystal\nHalite.get(\"http://httpbin.org/get\", params: { \"firstname\" =\u003e \"Olen\", \"lastname\" =\u003e \"Rosenbaum\" })\n```\n\n#### Form data\n\nUse the `form` argument to pass data serialized as form encoded:\n\n```crystal\nHalite.post(\"http://httpbin.org/post\", form: { \"firstname\" =\u003e \"Olen\", \"lastname\" =\u003e \"Rosenbaum\" })\n```\n\n#### File uploads (via form data)\n\nTo upload files as if form data, construct the form as follows:\n\n```crystal\nHalite.post(\"http://httpbin.org/post\", form: {\n  \"username\" =\u003e \"Quincy\",\n  \"avatar\" =\u003e File.open(\"/Users/icyleaf/quincy_avatar.png\")\n})\n```\n\nIt is possible to upload multiple files:\n\n```crystal\nHalite.post(\"http://httpbin.org/post\", form: {\n  photos: [\n    File.open(\"/Users/icyleaf/photo1.png\"),\n    File.open(\"/Users/icyleaf/photo2.png\")\n  ],\n  album_name: \"samples\"\n})\n```\n\nOr pass the name with `[]`:\n\n```crystal\nHalite.post(\"http://httpbin.org/post\", form: {\n  \"photos[]\" =\u003e [\n    File.open(\"/Users/icyleaf/photo1.png\"),\n    File.open(\"/Users/icyleaf/photo2.png\")\n  ],\n  \"album_name\" =\u003e \"samples\"\n})\n```\n\nMultiple files can also be uploaded using both ways above, it depend on web server.\n\n#### JSON data\n\nUse the `json` argument to pass data serialized as body encoded:\n\n```crystal\nHalite.post(\"http://httpbin.org/post\", json: { \"firstname\" =\u003e \"Olen\", \"lastname\" =\u003e \"Rosenbaum\" })\n```\n\n#### Raw String\n\nUse the `raw` argument to pass raw string as body and set the `Content-Type` manually:\n\n```crystal\n# Set content-type to \"text/plain\" by default\nHalite.post(\"http://httpbin.org/post\", raw: \"name=Peter+Lee\u0026address=%23123+Happy+Ave\u0026language=C%2B%2B\")\n\n# Set content-type manually\nHalite.post(\"http://httpbin.org/post\",\n  headers: {\n    \"content-type\" =\u003e \"application/json\"\n  },\n  raw: %Q{{\"name\":\"Peter Lee\",\"address\":\"23123 Happy Ave\",\"language\":\"C++\"}}\n)\n```\n\n### Passing advanced options\n\n#### Auth\n\nUse the `#basic_auth` method to perform [HTTP Basic Authentication](http://tools.ietf.org/html/rfc2617) using a username and password:\n\n```crystal\nHalite.basic_auth(user: \"user\", pass: \"p@ss\").get(\"http://httpbin.org/get\")\n\n# We can pass a raw authorization header using the auth method:\nHalite.auth(\"Bearer dXNlcjpwQHNz\").get(\"http://httpbin.org/get\")\n```\n\n#### User Agent\n\nUse the `#user_agent` method to overwrite default one:\n\n```crystal\nHalite.user_agent(\"Crystal Client\").get(\"http://httpbin.org/user-agent\")\n```\n\n#### Headers\n\nHere are two way to passing headers data:\n\n##### 1. Use the `#headers` method\n\n```crystal\nHalite.headers(private_token: \"T0k3n\").get(\"http://httpbin.org/get\")\n\n# Also support Hash or NamedTuple\nHalite.headers({ \"private_token\" =\u003e \"T0k3n\" }).get(\"http://httpbin.org/get\")\n\n# Or\nHalite.headers({ private_token: \"T0k3n\" }).get(\"http://httpbin.org/get\")\n```\n\n##### 2. Use the `headers` argument in the available request method:\n\n```crystal\nHalite.get(\"http://httpbin.org/anything\" , headers: { private_token: \"T0k3n\" })\n\nHalite.post(\"http://httpbin.org/anything\" , headers: { private_token: \"T0k3n\" })\n```\n\n#### Cookies\n\n##### Passing cookies in requests\n\nThe `Halite.cookies` option can be used to configure cookies for a given request:\n\n```crystal\nHalite.cookies(session_cookie: \"6abaef100b77808ceb7fe26a3bcff1d0\")\n      .get(\"http://httpbin.org/headers\")\n```\n\n##### Get cookies in requests\n\nTo obtain the cookies(cookie jar) for a given response, call the `#cookies` method:\n\n```crystal\nr = Halite.get(\"http://httpbin.org/cookies?set?session_cookie=6abaef100b77808ceb7fe26a3bcff1d0\")\npp r.cookies\n# =\u003e #\u003cHTTP::Cookies:0x10dbed980 @cookies={\"session_cookie\" =\u003e#\u003cHTTP::Cookie:0x10ec20f00 @domain=nil, @expires=nil, @extension=nil, @http_only=false, @name=\"session_cookie\", @path=\"/\", @secure=false, @value=\"6abaef100b77808ceb7fe26a3bcff1d0\"\u003e}\u003e\n```\n\n#### Redirects and History\n\n##### Automatically following redirects\n\nThe `Halite.follow` method can be used for automatically following redirects(Max up to 5 times):\n\n```crystal\n# Set the cookie and redirect to http://httpbin.org/cookies\nHalite.follow\n      .get(\"http://httpbin.org/cookies/set/name/foo\")\n```\n\n##### Limiting number of redirects\n\nAs above, set over 5 times, it will raise a `Halite::TooManyRedirectsError`, but you can change less if you can:\n\n```crystal\nHalite.follow(2)\n      .get(\"http://httpbin.org/relative-redirect/5\")\n```\n\n##### Disabling unsafe redirects\n\nIt only redirects with `GET`, `HEAD` request and returns a `300`, `301`, `302` by default, otherwise it will raise a `Halite::StateError`.\nWe can disable it to set `:strict` to `false` if we want any method(verb) requests, in which case the `GET` method(verb) will be used for\nthat redirect:\n\n```crystal\nHalite.follow(strict: false)\n      .post(\"http://httpbin.org/relative-redirect/5\")\n```\n\n##### History\n\n`Response#history` property list contains the `Response` objects that were created in order to complete the request.\nThe list is ordered from the oldest to most recent response.\n\n```crystal\nr = Halite.follow\n          .get(\"http://httpbin.org/redirect/3\")\n\nr.uri\n# =\u003e http://httpbin.org/get\n\nr.status_code\n# =\u003e 200\n\nr.history\n# =\u003e [\n#      #\u003cHalite::Response HTTP/1.1 302 FOUND {\"Location\" =\u003e \"/relative-redirect/2\" ...\u003e,\n#      #\u003cHalite::Response HTTP/1.1 302 FOUND {\"Location\" =\u003e \"/relative-redirect/1\" ...\u003e,\n#      #\u003cHalite::Response HTTP/1.1 302 FOUND {\"Location\" =\u003e \"/get\" ...\u003e,\n#      #\u003cHalite::Response HTTP/1.1 200 OK    {\"Content-Type\" =\u003e \"application/json\" ...\u003e\n#    ]\n```\n\n**NOTE**: It contains the `Response` object if you use `history` and HTTP was not a `30x`, For example:\n\n```crystal\nr = Halite.get(\"http://httpbin.org/get\")\nr.history.size # =\u003e 0\n\nr = Halite.follow\n          .get(\"http://httpbin.org/get\")\nr.history.size # =\u003e 1\n```\n\n#### Timeout\n\nBy default, the Halite does not enforce timeout on a request.\nWe can enable per operation timeouts by configuring them through the chaining API.\n\nThe `connect` timeout is the number of seconds Halite will wait for our client to establish a connection to a remote server call on the socket.\n\nOnce our client has connected to the server and sent the HTTP request,\nthe `read` timeout is the number of seconds the client will wait for the server to send a response.\n\n```crystal\n# Separate set connect and read timeout\nHalite.timeout(connect: 3.0, read: 2.minutes)\n      .get(\"http://httpbin.org/anything\")\n\n# Boath set connect and read timeout\n# The timeout value will be applied to both the connect and the read timeouts.\nHalite.timeout(5)\n      .get(\"http://httpbin.org/anything\")\n```\n\n### HTTPS\n\nThe Halite supports HTTPS via Crystal's built-in OpenSSL module. All you have to do in order to use HTTPS is pass in an https://-prefixed URL.\n\nTo use client certificates, you can pass in a custom `OpenSSL::SSL::Context::Client` object containing the certificates you wish to use:\n\n```crystal\ntls = OpenSSL::SSL::Context::Client.new\ntls.ca_certificates = File.expand_path(\"~/client.crt\")\ntls.private_key = File.expand_path(\"~/client.key\")\n\nHalite.get(\"https://httpbin.org/anything\", tls: tls)\n```\n\n### Response Handling\n\nAfter an HTTP request, `Halite::Response` object have several useful methods. (Also see the [API documentation](https://icyleaf.github.io/halite/Halite/Response.html)).\n\n- **#body**: The response body.\n- **#body_io**: The response body io only available in streaming requests.\n- **#status_code**: The HTTP status code.\n- **#content_type**: The content type of the response.\n- **#content_length**: The content length of the response.\n- **#cookies**: A `HTTP::Cookies` set by server.\n- **#headers**: A `HTTP::Headers` of the response.\n- **#links**: A list of `Halite::HeaderLink` set from headers.\n- **#parse**: (return value depends on MIME type) parse the body using a parser defined for the `#content_type`.\n- **#to_a**: Return a `Hash` of status code, response headers and body as a string.\n- **#to_raw**: Return a raw of response as a string.\n- **#to_s**: Return response body as a string.\n- **#version**: The HTTP version.\n\n#### Response Content\n\nWe can read the content of the server's response by call `#body`:\n\n```crystal\nr = Halite.get(\"http://httpbin.org/user-agent\")\nr.body\n# =\u003e {\"user-agent\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36\"}\n```\n\nThe `gzip` and `deflate` transfer-encodings are automatically decoded for you.\nAnd requests will automatically decode content from the server. Most unicode charsets are seamlessly decoded.\n\n#### JSON Content\n\nThere’s also a built-in a JSON adapter, in case you’re dealing with JSON data:\n\n```crystal\nr = Halite.get(\"http://httpbin.org/user-agent\")\nr.parse(\"json\")\nr.parse # simplily by default\n# =\u003e {\n# =\u003e   \"user-agent\" =\u003e \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36\"\n# =\u003e }\n```\n\n#### Parsing Content\n\n`Halite::Response` has a MIME type adapter system that you can use a decoder to parse the content,\nwe can inherit `Halite::MimeTypes::Adapter` to make our adapter:\n\n```crystal\n# Define a MIME type adapter\nclass YAMLAdapter \u003c Halite::MimeType::Adapter\n  def decode(string)\n    YAML.parse(string)\n  end\n\n  def encode(obj)\n    obj.to_yaml\n  end\nend\n\n# Register to Halite to invoke\nHalite::MimeType.register YAMLAdapter.new, \"application/x-yaml\", \"yaml\", \"yml\"\n\n# Test it!\nr = Halite.get \"https://raw.githubusercontent.com/icyleaf/halite/master/shard.yml\"\nr.parse(\"yaml\") # or \"yml\"\n# =\u003e {\"name\" =\u003e \"halite\", \"version\" =\u003e \"0.4.0\", \"authors\" =\u003e [\"icyleaf \u003cicyleaf.cn@gmail.com\u003e\"], \"crystal\" =\u003e \"0.25.0\", \"license\" =\u003e \"MIT\"}\n```\n\n#### Binary Data\n\nStore binary data (eg, `application/octet-stream`) to file, you can use [streaming requests](#streaming-requests):\n\n```crystal\nHalite.get(\"https://github.com/icyleaf/halite/archive/master.zip\") do |response|\n  filename = response.filename || \"halite-master.zip\"\n  File.open(filename, \"w\") do |file|\n    IO.copy(response.body_io, file)\n  end\nend\n```\n\n### Error Handling\n\n- For any status code, a `Halite::Response` will be returned.\n- If request timeout, a `Halite::TimeoutError` will be raised.\n- If a request exceeds the configured number of maximum redirections, a `Halite::TooManyRedirectsError` will raised.\n- If request uri is http and configured tls context, a `Halite::RequestError` will raised.\n- If request uri is invalid, a `Halite::ConnectionError`/`Halite::UnsupportedMethodError`/`Halite::UnsupportedSchemeError` will raised.\n\n#### Raise for status code\n\nIf we made a bad request(a 4xx client error or a 5xx server error response), we can raise with `Halite::Response.raise_for_status`.\n\nBut, since our `status_code` was not `4xx` or `5xx`, it returns `nil` when we call it:\n\n```crystal\nurls = [\n  \"https://httpbin.org/status/404\",\n  \"https://httpbin.org/status/500?foo=bar\",\n  \"https://httpbin.org/status/200\",\n]\n\nurls.each do |url|\n  r = Halite.get url\n  begin\n    r.raise_for_status\n    p r.body\n  rescue ex : Halite::ClientError | Halite::ServerError\n    p \"[#{ex.status_code}] #{ex.status_message} (#{ex.class})\"\n  end\nend\n\n# =\u003e \"[404] not found error with url: https://httpbin.org/status/404 (Halite::Exception::ClientError)\"\n# =\u003e \"[500] internal server error error with url: https://httpbin.org/status/500?foo=bar (Halite::Exception::ServerError)\"\n# =\u003e \"\"\n```\n\n## Middleware\n\nHalite now has middleware (a.k.a features) support providing a simple way to plug in intermediate custom logic\nin your HTTP client, allowing you to monitor outgoing requests, incoming responses, and use it as an interceptor.\n\nAvailable features:\n\n- [Logging](#logging) (Yes, logging is based on feature, cool, aha!)\n- [Local Cache](#local-cache) (local storage, speed up in development)\n\n### Write a simple feature\n\nLet's implement simple middleware that prints each request:\n\n```crystal\nclass RequestMonister \u003c Halite::Feature\n  @label : String\n  def initialize(**options)\n    @label = options.fetch(:label, \"\")\n  end\n\n  def request(request) : Halite::Request\n    puts @label\n    puts request.verb\n    puts request.uri\n    puts request.body\n\n    request\n  end\n\n  Halite.register_feature \"request_monster\", self\nend\n```\n\nThen use it in Halite:\n\n```crystal\nHalite.use(\"request_monster\", label: \"testing\")\n      .post(\"http://httpbin.org/post\", form: {name: \"foo\"})\n\n# Or configure to client\nclient = Halite::Client.new do\n  use \"request_monster\", label: \"testing\"\nend\n\nclient.post(\"http://httpbin.org/post\", form: {name: \"foo\"})\n\n# =\u003e testing\n# =\u003e POST\n# =\u003e http://httpbin.org/post\n# =\u003e name=foo\n```\n\n### Write a interceptor\n\nHalite's killer feature is the **interceptor**, Use `Halite::Feature::Chain` to process with two result:\n\n- `next`: perform and run next interceptor\n- `return`: perform and return\n\nSo, you can intercept and turn to the following registered features.\n\n```crystal\nclass AlwaysNotFound \u003c Halite::Feature\n  def intercept(chain)\n    response = chain.perform\n    response = Halite::Response.new(chain.request.uri, 404, response.body, response.headers)\n    chain.next(response)\n  end\n\n  Halite.register_feature \"404\", self\nend\n\nclass PoweredBy \u003c Halite::Feature\n  def intercept(chain)\n    if response = chain.response\n      response.headers[\"X-Powered-By\"] = \"Halite\"\n      chain.return(response)\n    else\n      chain\n    end\n  end\n\n  Halite.register_feature \"powered_by\", self\nend\n\nr = Halite.use(\"404\").use(\"powered_by\").get(\"http://httpbin.org/user-agent\")\nr.status_code               # =\u003e 404\nr.headers[\"X-Powered-By\"]   # =\u003e Halite\nr.body                      # =\u003e {\"user-agent\":\"Halite/0.6.0\"}\n```\n\nFor more implementation details about the feature layer, see the [Feature](https://github.com/icyleaf/halite/blob/master/src/halite/feature.cr#L2) class and [examples](https://github.com/icyleaf/halite/tree/master/src/halite/features) and [specs](https://github.com/icyleaf/halite/blob/master/spec/spec_helper.cr#L23).\n\n## Advanced Usage\n\n### Configuring\n\nHalite provides a traditional way to instance client, and you can configure any chainable methods with block:\n\n```crystal\nclient = Halite::Client.new do\n  # Set basic auth\n  basic_auth \"username\", \"password\"\n\n  # Enable logging\n  logging true\n\n  # Set timeout\n  timeout 10.seconds\n\n  # Set user agent\n  headers user_agent: \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36\"\nend\n\n# You also can configure in this way\nclient.accept(\"application/json\")\n\nr = client.get(\"http://httpbin.org/get\")\n```\n\n### Endpoint\n\nNo more given endpoint per request, use `endpoint` will make the request URI shorter, you can set it in flexible way:\n\n```crystal\nclient = Halite::Client.new do\n  endpoint \"https://gitlab.org/api/v4\"\n  user_agent \"Halite\"\nend\n\nclient.get(\"users\")       # GET https://gitlab.org/api/v4/users\n\n# You can override the path by using an absolute path\nclient.get(\"/users\")      # GET https://gitlab.org/users\n```\n\n### Sessions\n\nAs like [requests.Session()](http://docs.python-requests.org/en/master/user/advanced/#session-objects), Halite built-in session by default.\n\nLet's persist some cookies across requests:\n\n```crystal\nclient = Halite::Client.new\nclient.get(\"http://httpbin.org/cookies/set?private_token=6abaef100b77808ceb7fe26a3bcff1d0\")\nclient.get(\"http://httpbin.org/cookies\")\n# =\u003e 2018-06-25 18:41:05 +08:00 | request | GET    | http://httpbin.org/cookies/set?private_token=6abaef100b77808ceb7fe26a3bcff1d0\n# =\u003e 2018-06-25 18:41:06 +08:00 | response | 302    | http://httpbin.org/cookies/set?private_token=6abaef100b77808ceb7fe26a3bcff1d0 | text/html\n# =\u003e \u003c!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\"\u003e\n# =\u003e \u003ctitle\u003eRedirecting...\u003c/title\u003e\n# =\u003e \u003ch1\u003eRedirecting...\u003c/h1\u003e\n# =\u003e \u003cp\u003eYou should be redirected automatically to target URL: \u003ca href=\"/cookies\"\u003e/cookies\u003c/a\u003e.  If not click the link.\n# =\u003e 2018-06-25 18:41:06 +08:00 | request | GET    | http://httpbin.org/cookies\n# =\u003e 2018-06-25 18:41:07 +08:00 | response | 200    | http://httpbin.org/cookies | application/json\n# =\u003e {\"cookies\":{\"private_token\":\"6abaef100b77808ceb7fe26a3bcff1d0\"}}\n```\n\nAll it support with [chainable methods](https://icyleaf.github.io/halite/Halite/Chainable.html) in the other examples list in [requests.Session](http://docs.python-requests.org/en/master/user/advanced/#session-objects).\n\nNote, however, that chainable methods will not be persisted across requests, even if using a session. This example will only send the cookies or headers with the first request, but not the second:\n\n```crystal\nclient = Halite::Client.new\nr = client.cookies(\"username\": \"foobar\").get(\"http://httpbin.org/cookies\")\nr.body # =\u003e {\"cookies\":{\"username\":\"foobar\"}}\n\nr = client.get(\"http://httpbin.org/cookies\")\nr.body # =\u003e {\"cookies\":{}}\n```\n\nIf you want to manually add cookies, headers (even features etc) to your session, use the methods start with `with_` in `Halite::Options`\nto manipulate them:\n\n```crystal\nr = client.get(\"http://httpbin.org/cookies\")\nr.body # =\u003e {\"cookies\":{}}\n\nclient.options.with_cookie(\"username\": \"foobar\")\nr = client.get(\"http://httpbin.org/cookies\")\nr.body # =\u003e {\"cookies\":{\"username\":\"foobar\"}}\n```\n\n### Streaming Requests\n\nSimilar to [HTTP::Client](https://crystal-lang.org/api/0.36.1/HTTP/Client.html#streaming) usage with a block,\nyou can easily use same way, but Halite returns a `Halite::Response` object:\n\n```crystal\nr = Halite.get(\"http://httpbin.org/stream/5\") do |response|\n  response.status_code                  # =\u003e 200\n  response.body_io.each_line do |line|\n    puts JSON.parse(line)               # =\u003e {\"url\" =\u003e \"http://httpbin.org/stream/5\", \"args\" =\u003e {}, \"headers\" =\u003e {\"Host\" =\u003e \"httpbin.org\", \"Connection\" =\u003e \"close\", \"User-Agent\" =\u003e \"Halite/0.8.0\", \"Accept\" =\u003e \"*/*\", \"Accept-Encoding\" =\u003e \"gzip, deflate\"}, \"id\" =\u003e 0_i64}\n  end\nend\n```\n\n\u003e **Warning**:\n\u003e\n\u003e `body_io` is avaiabled as an `IO` and not reentrant safe. Might throws a \"Nil assertion failed\" exception if there is no data in the `IO`\n(such like `head` requests). Calling this method multiple times causes some of the received data being lost.\n\u003e\n\u003e One more thing, use streaming requests the response will always [enable redirect](#redirects-and-history) automatically.\n\n### Logging\n\nHalite does not enable logging on each request and response too.\nWe can enable per operation logging by configuring them through the chaining API.\n\nBy default, Halite will logging all outgoing HTTP requests and their responses(without binary stream) to `STDOUT` on DEBUG level.\nYou can configuring the following options:\n\n- `logging`: Instance your `Halite::Logging::Abstract`, check [Use the custom logging](#use-the-custom-logging).\n- `format`: Output format, built-in `common` and `json`, you can write your own.\n- `file`: Write to file with path, works with `format`.\n- `filemode`: Write file mode, works with `format`, by default is `a`. (append to bottom, create it if file is not exist)\n- `skip_request_body`: By default is `false`.\n- `skip_response_body`: By default is `false`.\n- `skip_benchmark`: Display elapsed time, by default is `false`.\n- `colorize`: Enable colorize in terminal, only apply in `common` format, by default is `true`.\n\n\u003e **NOTE**: `format` (`file` and `filemode`) and `logging` are conflict, you can not use both.\n\nLet's try with it:\n\n```crystal\n# Logging json request\nHalite.logging\n      .get(\"http://httpbin.org/get\", params: {name: \"foobar\"})\n\n# =\u003e 2018-06-25 18:33:14 +08:00 | request  | GET    | http://httpbin.org/get?name=foobar\n# =\u003e 2018-06-25 18:33:15 +08:00 | response | 200    | http://httpbin.org/get?name=foobar | 381.32ms | application/json\n# =\u003e {\"args\":{\"name\":\"foobar\"},\"headers\":{\"Accept\":\"*/*\",\"Accept-Encoding\":\"gzip, deflate\",\"Connection\":\"close\",\"Host\":\"httpbin.org\",\"User-Agent\":\"Halite/0.3.2\"},\"origin\":\"60.206.194.34\",\"url\":\"http://httpbin.org/get?name=foobar\"}\n\n# Logging image request\nHalite.logging\n      .get(\"http://httpbin.org/image/png\")\n\n# =\u003e 2018-06-25 18:34:15 +08:00 | request  | GET    | http://httpbin.org/image/png\n# =\u003e 2018-06-25 18:34:15 +08:00 | response | 200    | http://httpbin.org/image/png | image/png\n\n# Logging with options\nHalite.logging(skip_request_body: true, skip_response_body: true)\n      .post(\"http://httpbin.org/get\", form: {image: File.open(\"halite-logo.png\")})\n\n# =\u003e 2018-08-28 14:33:19 +08:00 | request  | POST   | http://httpbin.org/post\n# =\u003e 2018-08-28 14:33:21 +08:00 | response | 200    | http://httpbin.org/post | 1.61s | application/json\n```\n\n#### JSON-formatted logging\n\nIt has JSON formatted for developer friendly logging.\n\n```\nHalite.logging(format: \"json\")\n      .get(\"http://httpbin.org/get\", params: {name: \"foobar\"})\n```\n\n#### Write to a log file\n\n```crystal\n# Write plain text to a log file\nLog.setup(\"halite.file\", backend: Log::IOBackend.new(File.open(\"/tmp/halite.log\", \"a\")))\nHalite.logging(for: \"halite.file\", skip_benchmark: true, colorize: false)\n      .get(\"http://httpbin.org/get\", params: {name: \"foobar\"})\n\n# Write json data to a log file\nLog.setup(\"halite.file\", backend: Log::IOBackend.new(File.open(\"/tmp/halite.log\", \"a\")))\nHalite.logging(format: \"json\", for: \"halite.file\")\n      .get(\"http://httpbin.org/get\", params: {name: \"foobar\"})\n\n# Redirect *all* logging from Halite to a file:\nLog.setup(\"halite\", backend: Log::IOBackend.new(File.open(\"/tmp/halite.log\", \"a\")))\n```\n\n#### Use the custom logging\n\nCreating the custom logging by integration `Halite::Logging::Abstract` abstract class.\nHere has two methods must be implement: `#request` and `#response`.\n\n```crystal\nclass CustomLogging \u003c Halite::Logging::Abstract\n  def request(request)\n    @logger.info { \"| \u003e\u003e | %s | %s %s\" % [request.verb, request.uri, request.body] }\n  end\n\n  def response(response)\n    @logger.info { \"| \u003c\u003c | %s | %s %s\" % [response.status_code, response.uri, response.content_type] }\n  end\nend\n\n# Add to adapter list (optional)\nHalite::Logging.register \"custom\", CustomLogging.new\n\nHalite.logging(logging: CustomLogging.new)\n      .get(\"http://httpbin.org/get\", params: {name: \"foobar\"})\n\n# We can also call it use format name if you added it.\nHalite.logging(format: \"custom\")\n      .get(\"http://httpbin.org/get\", params: {name: \"foobar\"})\n\n# =\u003e 2017-12-13 16:40:13 +08:00 | \u003e\u003e | GET | http://httpbin.org/get?name=foobar\n# =\u003e 2017-12-13 16:40:15 +08:00 | \u003c\u003c | 200 | http://httpbin.org/get?name=foobar application/json\n```\n\n### Local Cache\n\nLocal cache feature is caching responses easily with Halite through an chainable method that is simple and elegant\nyet powerful. Its aim is to focus on the HTTP part of caching and do not worrying about how stuff stored, api rate limiting\neven works without network(offline).\n\nIt has the following options:\n\n- `file`: Load cache from file. it conflict with `path` and `expires`.\n- `path`: The path of cache, default is \"/tmp/halite/cache/\"\n- `expires`: The expires time of cache, default is never expires.\n- `debug`: The debug mode of cache, default is `true`\n\nWith debug mode, cached response it always included some headers information:\n\n- `X-Halite-Cached-From`: Cache source (cache or file)\n- `X-Halite-Cached-Key`: Cache key with verb, uri and body (return with cache, not `file` passed)\n- `X-Halite-Cached-At`:  Cache created time\n- `X-Halite-Cached-Expires-At`: Cache expired time (return with cache, not `file` passed)\n\n```crystal\nHalite.use(\"cache\").get \"http://httpbin.org/anything\"     # request a HTTP\nr = Halite.use(\"cache\").get \"http://httpbin.org/anything\" # request from local storage\nr.headers                                                 # =\u003e {..., \"X-Halite-Cached-At\" =\u003e \"2018-08-30 10:41:14 UTC\", \"X-Halite-Cached-By\" =\u003e \"Halite\", \"X-Halite-Cached-Expires-At\" =\u003e \"2018-08-30 10:41:19 UTC\", \"X-Halite-Cached-Key\" =\u003e \"2bb155e6c8c47627da3d91834eb4249a\"}}\n```\n\n### Link Headers\n\nMany HTTP APIs feature [Link headers](https://tools.ietf.org/html/rfc5988). GitHub uses\nthese for [pagination](https://developer.github.com/v3/#pagination) in their API, for example:\n\n```crystal\nr = Halite.get \"https://api.github.com/users/icyleaf/repos?page=1\u0026per_page=2\"\nr.links\n# =\u003e {\"next\" =\u003e\n# =\u003e   Halite::HeaderLink(\n# =\u003e    @params={},\n# =\u003e    @rel=\"next\",\n# =\u003e    @target=\"https://api.github.com/user/17814/repos?page=2\u0026per_page=2\"),\n# =\u003e  \"last\" =\u003e\n# =\u003e   Halite::HeaderLink(\n# =\u003e    @params={},\n# =\u003e    @rel=\"last\",\n# =\u003e    @target=\"https://api.github.com/user/17814/repos?page=41\u0026per_page=2\")}\n\nr.links[\"next\"]\n# =\u003e \"https://api.github.com/user/17814/repos?page=2\u0026per_page=2\"\n\nr.links[\"next\"].params\n# =\u003e {}\n```\n\n## Help and Discussion\n\nYou can browse the API documents:\n\nhttps://icyleaf.github.io/halite/\n\nYou can browse the all chainable methods:\n\nhttps://icyleaf.github.io/halite/Halite/Chainable.html\n\nYou can browse the Changelog:\n\nhttps://github.com/icyleaf/halite/blob/master/CHANGELOG.md\n\nIf you have found a bug, please create a issue here:\n\nhttps://github.com/icyleaf/halite/issues/new\n\n## How to Contribute\n\nYour contributions are always welcome! Please submit a pull request or create an issue to add a new question, bug or feature to the list.\n\nAll [Contributors](https://github.com/icyleaf/halite/graphs/contributors) are on the wall.\n\n## You may also like\n\n- [totem](https://github.com/icyleaf/totem) - Load and parse a configuration file or string in JSON, YAML, dotenv formats.\n- [markd](https://github.com/icyleaf/markd) - Yet another markdown parser built for speed, Compliant to CommonMark specification.\n- [poncho](https://github.com/icyleaf/poncho) - A .env parser/loader improved for performance.\n- [popcorn](https://github.com/icyleaf/popcorn) - Easy and Safe casting from one type to another.\n- [fast-crystal](https://github.com/icyleaf/fast-crystal) - 💨 Writing Fast Crystal 😍 -- Collect Common Crystal idioms.\n\n## License\n\n[MIT License](https://github.com/icyleaf/halite/blob/master/LICENSE) © icyleaf\n","funding_links":[],"categories":["HTTP","\u003ca name=\"Crystal\"\u003e\u003c/a\u003eCrystal"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ficyleaf%2Fhalite","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ficyleaf%2Fhalite","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ficyleaf%2Fhalite/lists"}