defmodule Req do @moduledoc ~S""" The high-level API. Req is composed of three main pieces: * `Req` - the high-level API (you're here!) * `Req.Request` - the low-level API and the request struct * `Req.Steps` - the collection of built-in steps The high-level API is what most users of Req will use most of the time. ## Examples Making a GET request with `Req.get!/1`: iex> Req.get!("https://api.github.com/repos/wojtekmach/req").body["description"] "Req is a batteries-included HTTP client for Elixir." Same, but by explicitly building request struct first: iex> req = Req.new(base_url: "https://api.github.com") iex> Req.get!(req, url: "/repos/wojtekmach/req").body["description"] "Req is a batteries-included HTTP client for Elixir." Making a POST request with `Req.post!/2`: iex> Req.post!("https://httpbin.org/post", form: [comments: "hello!"]).body["form"] %{"comments" => "hello!"} Stream request body: iex> stream = Stream.duplicate("foo", 3) iex> Req.post!("https://httpbin.org/post", body: stream).body["data"] "foofoofoo" Stream response body using a callback: iex> resp = ...> Req.get!("http://httpbin.org/stream/2", into: fn {:data, data}, {req, resp} -> ...> IO.puts(data) ...> {:cont, {req, resp}} ...> end) # output: {"url": "http://httpbin.org/stream/2", ...} # output: {"url": "http://httpbin.org/stream/2", ...} iex> resp.status 200 iex> resp.body "" Stream response body into a `Collectable`: iex> resp = Req.get!("http://httpbin.org/stream/2", into: IO.stream()) # output: {"url": "http://httpbin.org/stream/2", ...} # output: {"url": "http://httpbin.org/stream/2", ...} iex> resp.status 200 iex> resp.body %IO.Stream{} ## Header Names The HTTP specification requires that header names should be case-insensitive. Req allows two ways to access the headers; using functions and by accessing the data directly: iex> Req.Response.get_header(response, "content-type") ["text/html"] iex> response.headers["content-type"] ["text/html"] While we can ensure case-insensitive handling in the former case, we can't in the latter. For this reason, Req made the following design choices: * header names are stored as downcased * functions like `Req.Request.get_header/2`, `Req.Request.put_header/3`, `Req.Response.get_header/2`, `Req.Response.put_header/3`, etc automatically downcase the given header name. """ # TODO: Wait for Finch 0.17 # Response streaming to caller: # # iex> {req, resp} = Req.async_request!("http://httpbin.org/stream/2") # iex> resp.status # 200 # iex> resp.body # "" # iex> Req.parse_message(req, receive do message -> message end) # [{:data, "{\"url\": \"http://httpbin.org/stream/2\"" <> ...}] # iex> Req.parse_message(req, receive do message -> message end) # [{:data, "{\"url\": \"http://httpbin.org/stream/2\"" <> ...}] # iex> Req.parse_message(req, receive do message -> message end) # [:done] # "" @type url() :: URI.t() | String.t() @doc """ Returns a new request struct with built-in steps. See `Req.Request` module documentation for more information on the underlying request struct. ## Options Basic request options: * `:method` - the request method, defaults to `:get`. * `:url` - the request URL. * `:headers` - the request headers as a `{key, value}` enumerable (e.g. map, keyword list). The header names should be downcased. The headers are automatically encoded using these rules: * atom header names are turned into strings, replacing `_` with `-`. For example, `:user_agent` becomes `"user-agent"`. * string header names are downcased. * `%DateTime{}` header values are encoded as "HTTP date". * other header values are encoded with `String.Chars.to_string/1`. If you set `:headers` options both in `Req.new/1` and `request/2`, the header lists are merged. See also "Header Names" section in the module documentation. * `:body` - the request body. Can be one of: * `iodata` - send request body eagerly * `enumerable` - stream `enumerable` as request body Additional URL options: * `:base_url` - if set, the request URL is prepended with this base URL (via [`put_base_url`](`Req.Steps.put_base_url/1`) step.) * `:params` - if set, appends parameters to the request query string (via [`put_params`](`Req.Steps.put_params/1`) step.) * `:path_params` - if set, uses a templated request path (via [`put_path_params`](`Req.Steps.put_path_params/1`) step.) Authentication options: * `:auth` - sets request authentication (via [`auth`](`Req.Steps.auth/1`) step.) Can be one of: * `{:basic, userinfo}` - uses Basic HTTP authentication. * `{:bearer, token}` - uses Bearer HTTP authentication. * `:netrc` - load credentials from the default .netrc file. * `{:netrc, path}` - load credentials from `path`. * `string` - sets to this value. * `:redact_auth` - if set to `true`, when `Req.Request` struct is inspected, authentication credentials are redacted. Defaults to `true`. Request body options: * `:form` - if set, encodes the request body as form data ([`encode_body`](`Req.Steps.encode_body/1`) step.) * `:json` - if set, encodes the request body as JSON ([`encode_body`](`Req.Steps.encode_body/1`) step.) * `:compress_body` - if set to `true`, compresses the request body using gzip (via [`compress_body`](`Req.Steps.compress_body/1`) step.) Defaults to `false`. Response body options: * `:compressed` - if set to `true`, asks the server to return compressed response. (via [`compressed`](`Req.Steps.compressed/1`) step.) Defaults to `true`. * `:raw` - if set to `true`, disables automatic body decompression ([`decompress_body`](`Req.Steps.decompress_body/1`) step) and decoding ([`decode_body`](`Req.Steps.decode_body/1`) step.) Defaults to `false`. * `:decode_body` - if set to `false`, disables automatic response body decoding. Defaults to `true`. * `:decode_json` - options to pass to `Jason.decode!/2`, defaults to `[]`. * `:into` - where to send the response body. It can be one of: * `nil` - (default) read the whole response body and store it in the `response.body` field. * `fun` - stream response body using a function. The first argument is a `{:data, data}` tuple containing the chunk of the response body. The second argument is a `{request, response}` tuple. For example: into: fn {:data, data}, {req, resp} -> IO.puts(data) {:cont, {req, resp}} end * `collectable` - stream response body into a `t:Collectable.t/0`. Response redirect options ([`redirect`](`Req.Steps.redirect/1`) step): * `:redirect` - if set to `false`, disables automatic response redirects. Defaults to `true`. * `:redirect_trusted` - by default, authorization credentials are only sent on redirects with the same host, scheme and port. If `:redirect_trusted` is set to `true`, credentials will be sent to any host. * `:max_redirects` - the maximum number of redirects, defaults to `10`. Retry options ([`retry`](`Req.Steps.retry/1`) step): * `:retry` - can be one of the following: * `:safe_transient` (default) - retry safe (GET/HEAD) requests on HTTP 408/429/500/502/503/504 responses or exceptions with `reason` field set to `:timeout`/`:econnrefused`/`:closed`. * `:transient` - same as `:safe_transient` except retries all HTTP methods (POST, DELETE, etc.) * `fun` - a 2-arity function that accepts a `Req.Request` and either a `Req.Response` or an exception struct and returns one of the following: * `true` - retry with the default delay controller by default delay option described below. * `{:delay, milliseconds}` - retry with the given delay. * `false/nil` - don't retry. * `false` - don't retry. * `:retry_delay` - if not set, which is the default, the retry delay is determined by the value of `retry-delay` header on HTTP 429/503 responses. If the header is not set, the default delay follows a simple exponential backoff: 1s, 2s, 4s, 8s, ... `:retry_delay` can be set to a function that receives the retry count (starting at 0) and returns the delay, the number of milliseconds to sleep before making another attempt. * `:retry_log_level` - the log level to emit retry logs at. Can also be set to `false` to disable logging these messages. Defaults to `:error`. * `:max_retries` - maximum number of retry attempts, defaults to `3` (for a total of `4` requests to the server, including the initial one.) Caching options ([`cache`](`Req.Steps.cache/1`) step): * `:cache` - if `true`, performs HTTP caching. Defaults to `false`. * `:cache_dir` - the directory to store the cache, defaults to `/req` (see: `:filename.basedir/3`) Request adapters: * `:adapter` - adapter to use to make the actual HTTP request. See `:adapter` field description in the `Req.Request` module documentation for more information. Defaults to calling [`run_finch`](`Req.Steps.run_finch/1`). * `:plug` - if set, calls the given Plug instead of making an HTTP request over the network (via [`put_plug`](`Req.Steps.put_plug/1`) step). Finch options ([`run_finch`](`Req.Steps.run_finch/1`) step) * `:finch` - the Finch pool to use. Defaults to pool automatically started by `Req`. * `:connect_options` - dynamically starts (or re-uses already started) Finch pool with the given connection options: * `:timeout` - socket connect timeout in milliseconds, defaults to `30_000`. * `:protocol` - the HTTP protocol to use, defaults to `:http1`. * `:hostname` - Mint explicit hostname. * `:transport_opts` - Mint transport options. * `:proxy_headers` - Mint proxy headers. * `:proxy` - Mint HTTP/1 proxy settings, a `{schema, address, port, options}` tuple. * `:client_settings` - Mint HTTP/2 client settings. * `:inet6` - if set to true, uses IPv6. Defaults to `false`. * `:pool_timeout` - pool checkout timeout in milliseconds, defaults to `5000`. * `:receive_timeout` - socket receive timeout in milliseconds, defaults to `15_000`. * `:unix_socket` - if set, connect through the given UNIX domain socket. * `:finch_private` - a map or keyword list of private metadata to add to the Finch request. May be useful for adding custom data when handling telemetry with `Finch.Telemetry`. * `:finch_request` - a function that executes the Finch request, defaults to using `Finch.request/3`. ## Examples iex> req = Req.new(url: "https://elixir-lang.org") iex> req.method :get iex> URI.to_string(req.url) "https://elixir-lang.org" Fake adapter: iex> fake = fn request -> ...> {request, Req.Response.new(status: 200, body: "it works!")} ...> end iex> iex> req = Req.new(adapter: fake) iex> Req.get!(req).body "it works!" """ @spec new(options :: keyword()) :: Req.Request.t() def new(options \\ []) do options = Keyword.merge(default_options(), options) {plugins, options} = Keyword.pop(options, :plugins, []) %Req.Request{ registered_options: MapSet.new([ # request steps :user_agent, :compressed, :range, :base_url, :params, :path_params, :auth, :form, :json, :compress_body, :checksum, # response steps :raw, :http_errors, :decode_body, :decode_json, :redirect, :redirect_trusted, :redirect_log_level, :max_redirects, :retry, :retry_delay, :retry_log_level, :max_retries, :cache, :cache_dir, :plug, :finch, :finch_request, :finch_private, :connect_options, :inet6, :receive_timeout, :pool_timeout, :unix_socket, :redact_auth, # TODO: Remove on Req 1.0 :output, :follow_redirects, :location_trusted ]) } |> update(options) |> Req.Request.prepend_request_steps( put_user_agent: &Req.Steps.put_user_agent/1, compressed: &Req.Steps.compressed/1, encode_body: &Req.Steps.encode_body/1, put_base_url: &Req.Steps.put_base_url/1, auth: &Req.Steps.auth/1, put_params: &Req.Steps.put_params/1, put_path_params: &Req.Steps.put_path_params/1, put_range: &Req.Steps.put_range/1, cache: &Req.Steps.cache/1, put_plug: &Req.Steps.put_plug/1, compress_body: &Req.Steps.compress_body/1, checksum: &Req.Steps.checksum/1 ) |> Req.Request.prepend_response_steps( retry: &Req.Steps.retry/1, redirect: &Req.Steps.redirect/1, verify_checksum: &Req.Steps.verify_checksum/1, decompress_body: &Req.Steps.decompress_body/1, decode_body: &Req.Steps.decode_body/1, handle_http_errors: &Req.Steps.handle_http_errors/1, output: &Req.Steps.output/1 ) |> Req.Request.prepend_error_steps(retry: &Req.Steps.retry/1) |> run_plugins(plugins) end defp new(%Req.Request{} = request, options) when is_list(options) do Req.update(request, options) end defp new(options1, options2) when is_list(options1) and is_list(options2) do new(options1 ++ options2) end defp new(url, options) when (is_binary(url) or is_struct(url, URI)) and is_list(options) do new([url: url] ++ options) end defp new(request, options) when is_list(options) do raise ArgumentError, "expected 1st argument to be a request, got: #{inspect(request)}" end defp new(_request, options) do raise ArgumentError, "expected 2nd argument to be an options keywords list, got: #{inspect(options)}" end @doc """ Updates a request struct. See `new/1` for a list of available options. Also see `Req.Request` module documentation for more information on the underlying request struct. ## Examples iex> req = Req.new(base_url: "https://httpbin.org") iex> req = Req.update(req, auth: {:basic, "alice:secret"}) iex> req.options[:base_url] "https://httpbin.org" iex> req.options[:auth] {:basic, "alice:secret"} Passing `:headers` will automatically encode and merge them: iex> req = Req.new(headers: %{point_x: 1}) iex> req = Req.update(req, headers: %{point_y: 2}) iex> req.headers %{"point-x" => ["1"], "point-y" => ["2"]} The same header names are overwritten however: iex> req = Req.new(headers: %{authorization: "bearer foo"}) iex> req = Req.update(req, headers: %{authorization: "bearer bar"}) iex> req.headers %{"authorization" => ["bearer bar"]} Similarly to headers, `:params` are merged too: req = Req.new(url: "https://httpbin.org/anything", params: [a: 1, b: 1]) req = Req.update(req, params: [a: 2]) Req.get!(req).body["args"] #=> %{"a" => "2", "b" => "1"} """ @spec update(Req.Request.t(), options :: keyword()) :: Req.Request.t() def update(%Req.Request{} = request, options) when is_list(options) do request_option_names = [:method, :url, :headers, :body, :adapter, :into] {request_options, options} = Keyword.split(options, request_option_names) if options[:output] && unquote(!System.get_env("REQ_NOWARN_OUTPUT")) do IO.warn("setting `output: path` is deprecated in favour of `into: File.stream!(path)`") end registered = MapSet.union( request.registered_options, MapSet.new(request_option_names) ) Req.Request.validate_options(options, registered) request = Enum.reduce(request_options, request, fn {:url, url}, acc -> put_in(acc.url, URI.parse(url)) {:headers, new_headers}, acc -> update_in(acc.headers, fn old_headers -> if unquote(Req.MixProject.legacy_headers_as_lists?()) do new_headers = encode_headers(new_headers) new_header_names = Enum.map(new_headers, &elem(&1, 0)) Enum.reject(old_headers, &(elem(&1, 0) in new_header_names)) ++ new_headers else Map.merge(old_headers, encode_headers(new_headers)) end end) {name, value}, acc -> %{acc | name => value} end) update_in( request.options, &Map.merge(&1, Map.new(options), fn :params, old, new -> Keyword.merge(old, new) _, _, new -> new end) ) end @doc """ Makes a GET request and returns a response or an error. `request` can be one of: * a `String` or `URI`; * a `Keyword` options; * a `Req.Request` struct See `new/1` for a list of available options. ## Examples With URL: iex> {:ok, resp} = Req.get("https://api.github.com/repos/wojtekmach/req") iex> resp.body["description"] "Req is a batteries-included HTTP client for Elixir." With options: iex> {:ok, resp} = Req.get(url: "https://api.github.com/repos/wojtekmach/req") iex> resp.status 200 With request struct: iex> req = Req.new(base_url: "https://api.github.com") iex> {:ok, resp} = Req.get(req, url: "/repos/elixir-lang/elixir") iex> resp.status 200 """ @doc type: :request @spec get(url() | keyword() | Req.Request.t(), options :: keyword()) :: {:ok, Req.Response.t()} | {:error, Exception.t()} def get(request, options \\ []) do request(%{new(request, options) | method: :get}) end @doc """ Makes a GET request and returns a response or raises an error. `request` can be one of: * a `String` or `URI`; * a `Keyword` options; * a `Req.Request` struct See `new/1` for a list of available options. ## Examples With URL: iex> Req.get!("https://api.github.com/repos/wojtekmach/req").body["description"] "Req is a batteries-included HTTP client for Elixir." With options: iex> Req.get!(url: "https://api.github.com/repos/wojtekmach/req").status 200 With request struct: iex> req = Req.new(base_url: "https://api.github.com") iex> Req.get!(req, url: "/repos/elixir-lang/elixir").status 200 """ @doc type: :request @spec get!(url() | keyword() | Req.Request.t(), options :: keyword()) :: Req.Response.t() def get!(request, options \\ []) do request!(%{new(request, options) | method: :get}) end @doc """ Makes a HEAD request and returns a response or an error. `request` can be one of: * a `String` or `URI`; * a `Keyword` options; * a `Req.Request` struct See `new/1` for a list of available options. ## Examples With URL: iex> {:ok, resp} = Req.head("https://httpbin.org/status/201") iex> resp.status 201 With options: iex> {:ok, resp} = Req.head(url: "https://httpbin.org/status/201") iex> resp.status 201 With request struct: iex> req = Req.new(base_url: "https://httpbin.org") iex> {:ok, resp} = Req.head(req, url: "/status/201") iex> resp.status 201 """ @doc type: :request @spec head(url() | keyword() | Req.Request.t(), options :: keyword()) :: {:ok, Req.Response.t()} | {:error, Exception.t()} def head(request, options \\ []) do request(%{new(request, options) | method: :head}) end @doc """ Makes a HEAD request and returns a response or raises an error. `request` can be one of: * a `String` or `URI`; * a `Keyword` options; * a `Req.Request` struct See `new/1` for a list of available options. ## Examples With URL: iex> Req.head!("https://httpbin.org/status/201").status 201 With options: iex> Req.head!(url: "https://httpbin.org/status/201").status 201 With request struct: iex> req = Req.new(base_url: "https://httpbin.org") iex> Req.head!(req, url: "/status/201").status 201 """ @doc type: :request @spec head!(url() | keyword() | Req.Request.t(), options :: keyword()) :: Req.Response.t() def head!(request, options \\ []) do request!(%{new(request, options) | method: :head}) end @doc """ Makes a POST request and returns a response or an error. `request` can be one of: * a `String` or `URI`; * a `Keyword` options; * a `Req.Request` struct See `new/1` for a list of available options. ## Examples With URL: iex> {:ok, resp} = Req.post("https://httpbin.org/anything", body: "hello!") iex> resp.body["data"] "hello!" iex> {:ok, resp} = Req.post("https://httpbin.org/anything", form: [x: 1]) iex> resp.body["form"] %{"x" => "1"} iex> {:ok, resp} = Req.post("https://httpbin.org/anything", json: %{x: 2}) iex> resp.body["json"] %{"x" => 2} With options: iex> {:ok, resp} = Req.post(url: "https://httpbin.org/anything", body: "hello!") iex> resp.body["data"] "hello!" With request struct: iex> req = Req.new(url: "https://httpbin.org/anything") iex> {:ok, resp} = Req.post(req, body: "hello!") iex> resp.body["data"] "hello!" """ @doc type: :request @spec post(url() | keyword() | Req.Request.t(), options :: keyword()) :: {:ok, Req.Response.t()} | {:error, Exception.t()} def post(request, options \\ []) do request(%{new(request, options) | method: :post}) end @doc """ Makes a POST request and returns a response or raises an error. `request` can be one of: * a `String` or `URI`; * a `Keyword` options; * a `Req.Request` struct See `new/1` for a list of available options. ## Examples With URL: iex> Req.post!("https://httpbin.org/anything", body: "hello!").body["data"] "hello!" iex> Req.post!("https://httpbin.org/anything", form: [x: 1]).body["form"] %{"x" => "1"} iex> Req.post!("https://httpbin.org/anything", json: %{x: 2}).body["json"] %{"x" => 2} With options: iex> Req.post!(url: "https://httpbin.org/anything", body: "hello!").body["data"] "hello!" With request struct: iex> req = Req.new(url: "https://httpbin.org/anything") iex> Req.post!(req, body: "hello!").body["data"] "hello!" """ @doc type: :request @spec post!(url() | keyword() | Req.Request.t(), options :: keyword()) :: Req.Response.t() def post!(request, options \\ []) do request!(%{new(request, options) | method: :post}) end @doc """ Makes a PUT request and returns a response or an error. `request` can be one of: * a `String` or `URI`; * a `Keyword` options; * a `Req.Request` struct See `new/1` for a list of available options. ## Examples With URL: iex> {:ok, resp} = Req.put("https://httpbin.org/anything", body: "hello!") iex> resp.body["data"] "hello!" With options: iex> {:ok, resp} = Req.put(url: "https://httpbin.org/anything", body: "hello!") iex> resp.body["data"] "hello!" With request struct: iex> req = Req.new(url: "https://httpbin.org/anything") iex> {:ok, resp} = Req.put(req, body: "hello!") iex> resp.body["data"] "hello!" """ @doc type: :request @spec put(url() | keyword() | Req.Request.t(), options :: keyword()) :: {:ok, Req.Response.t()} | {:error, Exception.t()} def put(request, options \\ []) do request(%{new(request, options) | method: :put}) end @doc """ Makes a PUT request and returns a response or raises an error. `request` can be one of: * a `String` or `URI`; * a `Keyword` options; * a `Req.Request` struct See `new/1` for a list of available options. ## Examples With URL: iex> Req.put!("https://httpbin.org/anything", body: "hello!").body["data"] "hello!" With options: iex> Req.put!(url: "https://httpbin.org/anything", body: "hello!").body["data"] "hello!" With request struct: iex> req = Req.new(url: "https://httpbin.org/anything") iex> Req.put!(req, body: "hello!").body["data"] "hello!" """ @doc type: :request @spec put!(url() | keyword() | Req.Request.t(), options :: keyword()) :: Req.Response.t() def put!(request, options \\ []) do request!(%{new(request, options) | method: :put}) end @doc """ Makes a PATCH request and returns a response or an error. `request` can be one of: * a `String` or `URI`; * a `Keyword` options; * a `Req.Request` struct See `new/1` for a list of available options. ## Examples With URL: iex> {:ok, resp} = Req.patch("https://httpbin.org/anything", body: "hello!") iex> resp.body["data"] "hello!" With options: iex> {:ok, resp} = Req.patch(url: "https://httpbin.org/anything", body: "hello!") iex> resp.body["data"] "hello!" With request struct: iex> req = Req.new(url: "https://httpbin.org/anything") iex> {:ok, resp} = Req.patch(req, body: "hello!") iex> resp.body["data"] "hello!" """ @doc type: :request @spec patch(url() | keyword() | Req.Request.t(), options :: keyword()) :: {:ok, Req.Response.t()} | {:error, Exception.t()} def patch(request, options \\ []) do request(%{new(request, options) | method: :patch}) end @doc """ Makes a PATCH request and returns a response or raises an error. `request` can be one of: * a `String` or `URI`; * a `Keyword` options; * a `Req.Request` struct See `new/1` for a list of available options. ## Examples With URL: iex> Req.patch!("https://httpbin.org/anything", body: "hello!").body["data"] "hello!" With options: iex> Req.patch!(url: "https://httpbin.org/anything", body: "hello!").body["data"] "hello!" With request struct: iex> req = Req.new(url: "https://httpbin.org/anything") iex> Req.patch!(req, body: "hello!").body["data"] "hello!" """ @doc type: :request @spec patch!(url() | keyword() | Req.Request.t(), options :: keyword()) :: Req.Response.t() def patch!(request, options \\ []) do request!(%{new(request, options) | method: :patch}) end @doc """ Makes a DELETE request and returns a response or an error. `request` can be one of: * a `String` or `URI`; * a `Keyword` options; * a `Req.Request` struct See `new/1` for a list of available options. ## Examples With URL: iex> {:ok, resp} = Req.delete("https://httpbin.org/anything") iex> resp.body["method"] "DELETE" With options: iex> {:ok, resp} = Req.delete(url: "https://httpbin.org/anything") iex> resp.body["method"] "DELETE" With request struct: iex> req = Req.new(url: "https://httpbin.org/anything") iex> {:ok, resp} = Req.delete(req) iex> resp.body["method"] "DELETE" """ @doc type: :request @spec delete(url() | keyword() | Req.Request.t(), options :: keyword()) :: {:ok, Req.Response.t()} | {:error, Exception.t()} def delete(request, options \\ []) do request(%{new(request, options) | method: :delete}) end @doc """ Makes a DELETE request and returns a response or raises an error. `request` can be one of: * a `String` or `URI`; * a `Keyword` options; * a `Req.Request` struct See `new/1` for a list of available options. ## Examples With URL: iex> Req.delete!("https://httpbin.org/anything").body["method"] "DELETE" With options: iex> Req.delete!(url: "https://httpbin.org/anything").body["method"] "DELETE" With request struct: iex> req = Req.new(url: "https://httpbin.org/anything") iex> Req.delete!(req).body["method"] "DELETE" """ @doc type: :request @spec delete!(url() | keyword() | Req.Request.t(), options :: keyword()) :: Req.Response.t() def delete!(request, options \\ []) do request!(%{new(request, options) | method: :delete}) end @doc """ Makes an HTTP request and returns a response or an error. `request` can be one of: * a `Keyword` options; * a `Req.Request` struct See `new/1` for a list of available options. ## Examples With options keywords list: iex> {:ok, response} = Req.request(url: "https://api.github.com/repos/wojtekmach/req") iex> response.status 200 iex> response.body["description"] "Req is a batteries-included HTTP client for Elixir." With request struct: iex> req = Req.new(url: "https://api.github.com/repos/elixir-lang/elixir") iex> {:ok, response} = Req.request(req) iex> response.status 200 """ @doc type: :request @spec request(request :: Req.Request.t() | keyword(), options :: keyword()) :: {:ok, Req.Response.t()} | {:error, Exception.t()} def request(request, options \\ []) do Req.Request.run(new(request, options)) end @doc """ Makes an HTTP request and returns a response or raises an error. See `new/1` for a list of available options. ## Examples With options keywords list: iex> Req.request!(url: "https://api.github.com/repos/elixir-lang/elixir").status 200 With request struct: iex> req = Req.new(url: "https://api.github.com/repos/elixir-lang/elixir") iex> Req.request!(req).status 200 """ @doc type: :request @spec request!(request :: Req.Request.t() | keyword(), options :: keyword()) :: Req.Response.t() def request!(request, options \\ []) do case request(request, options) do {:ok, response} -> response {:error, exception} -> raise exception end end # TODO: Wait for Finch 0.17 @doc false def async_request(request, options \\ []) do Req.Request.run_request(%{new(request, options) | into: :self}) end # TODO: Wait for Finch 0.17 @doc false def async_request!(request, options \\ []) do case async_request(request, options) do {request, %Req.Response{} = response} -> {request, response} {_request, exception} -> raise exception end end # TODO: Wait for Finch 0.17 @doc false def parse_message(%Req.Request{} = request, message) do request.async.stream_fun.(request.async.ref, message) end # TODO: Wait for Finch 0.17 @doc false def cancel_async_request(%Req.Request{} = request) do request.async.cancel_fun.(request.async.ref) end # TODO: Req.run/2? # defp run_request(request, options \\ []) do # request # |> Req.update(options) # |> Req.Request.run_request() # end # defp run_request!(request, options \\ []) do # case run_request(request, options) do # {request, %Req.Response{} = response} -> # {request, response} # {_request, exception} -> # raise exception # end # end @doc """ Returns default options. See `default_options/1` for more information. """ @spec default_options() :: keyword() def default_options() do Application.get_env(:req, :default_options, []) end @doc """ Sets default options for `Req.new/1`. Avoid setting default options in libraries as they are global. ## Examples iex> Req.default_options(base_url: "https://httpbin.org") iex> Req.get!("/statuses/201").status 201 iex> Req.new() |> Req.get!(url: "/statuses/201").status 201 """ @spec default_options(keyword()) :: :ok def default_options(options) do Application.put_env(:req, :default_options, options) end if Req.MixProject.legacy_headers_as_lists?() do defp encode_headers(headers) do for {name, value} <- headers do {encode_header_name(name), encode_header_value(value)} end end else defp encode_headers(headers) do Enum.reduce(headers, %{}, fn {name, value}, acc -> Map.update( acc, encode_header_name(name), encode_header_values(List.wrap(value)), &(&1 ++ encode_header_values(List.wrap(value))) ) end) end defp encode_header_values([value | rest]) do [encode_header_value(value) | encode_header_values(rest)] end defp encode_header_values([]) do [] end end defp encode_header_name(name) when is_atom(name) do name |> Atom.to_string() |> String.replace("_", "-") |> __ensure_header_downcase__() end defp encode_header_name(name) when is_binary(name) do __ensure_header_downcase__(name) end defp encode_header_value(%DateTime{} = datetime) do datetime |> DateTime.shift_zone!("Etc/UTC") |> Req.Steps.format_http_datetime() end defp encode_header_value(%NaiveDateTime{} = datetime) do IO.warn("setting header to %NaiveDateTime{} is deprecated, use %DateTime{} instead") Req.Steps.format_http_datetime(datetime) end defp encode_header_value(value) do String.Chars.to_string(value) end # Plugins support is experimental, undocumented, and likely won't make the new release. defp run_plugins(request, [plugin | rest]) when is_atom(plugin) do run_plugins(plugin.attach(request), rest) end defp run_plugins(request, [plugin | rest]) when is_function(plugin, 1) do run_plugins(plugin.(request), rest) end defp run_plugins(request, []) do request end @doc false @deprecated "Manually build Req.Request struct instead" def build(method, url, options \\ []) do %Req.Request{ method: method, url: URI.parse(url), headers: Keyword.get(options, :headers, []), body: Keyword.get(options, :body, "") } end def __ensure_header_downcase__(name) do String.downcase(name, :ascii) end end