HTTP transport for the Duffel API.
Handles authentication, required headers, the data request/response
envelope, error normalisation and cursor pagination. Resource modules
(e.g. Duffel.OfferRequests) build on top of this module; most
applications won't need to call it directly.
Errors
A failed request always comes back as {:error, %Duffel.Error{}}. That
covers requests the API rejected and requests that never reached it: a
connection or timeout failure becomes an error with type: :transport_error and the original exception under :reason.
Timeouts
A request waits 130 seconds for a response before giving up. Duffel
allows order and booking creation to take up to 120 seconds, and
recommends a client timeout slightly above that. Searches are quicker:
each airline gets 20 seconds to answer by default, up to the 60 seconds
supplier_timeout allows. Lower :receive_timeout on a client used
only for searching:
Duffel.new(access_token: token, receive_timeout: 30_000)A timeout is a transient failure, so it is retried like any other.
Retries and idempotency
A failed request is retried up to three times with a growing delay, but
only when Duffel calls the failure retryable: a 408, 429 or 503, or a
network error. Duffel documents 500 and 502 as "you should not retry
this request", so neither is. A 504 can mean the supplier processed the
request after all, so it is retried for GET and HEAD only, never for
a POST that could book twice.
Retries still apply to every method, so each POST also carries an
Idempotency-Key header. Duffel's API documentation never mentions
idempotency keys, so treat the header as a precaution rather than a
promise that a repeated POST is discarded — what stops a retry booking
twice is the policy above. See post/4 for how to supply your own key.
Pass your own retry: in :req_options to replace this policy.
Telemetry
Every request emits a telemetry span
under the [:duffel, :request] prefix:
[:duffel, :request, :start]- measurements%{system_time, monotonic_time}[:duffel, :request, :stop]- measurements%{duration, monotonic_time}[:duffel, :request, :exception]- when the request function raises
Metadata on every event: :method, :path and :base_url. The :stop
event also carries :status (the HTTP status, or nil on a transport
error), :result (:ok or :error) and :rate_limit (a
Duffel.RateLimit, or nil when the response reported none).
Attach a handler to measure request latency:
:telemetry.attach(
"duffel-logger",
[:duffel, :request, :stop],
fn _event, %{duration: duration}, meta, _config ->
ms = System.convert_time_unit(duration, :native, :millisecond)
Logger.info("duffel #{meta.method} #{meta.path} -> #{meta.status} (#{ms}ms)")
end,
nil
)
Summary
Functions
Performs a DELETE request.
Throws away the response body, reporting only whether the request succeeded.
Performs a GET request.
Performs a GET request against a list endpoint and wraps the result
in a Duffel.Page.
Builds a client struct.
Performs a PATCH request, wrapping body in the data envelope.
Performs a POST request, wrapping body in the data envelope the
Duffel API expects.
Performs a PUT request, wrapping body in the data envelope.
Lazily streams every item from a paginated list endpoint, following
meta.after cursors until exhausted.
Takes the resource out of the data envelope Duffel wraps it in.
Types
Functions
Performs a DELETE request.
@spec discard(response()) :: :ok | {:error, Duffel.Error.t()}
Throws away the response body, reporting only whether the request succeeded.
For endpoints that return nothing useful, such as a delete or an action that just acknowledges. Errors pass through untouched.
Examples
client |> delete("/air/webhooks/sev_123") |> discard()
#=> :ok
Performs a GET request.
Options
:params- query string parameters. A list value is sent as one parameter per element, which is how Duffel'skey[]array filters work:params: %{"passenger_name[]" => ["Amelia", "Earhart"]}sendspassenger_name[]=Amelia&passenger_name[]=Earhart.
@spec get_data(t(), String.t(), keyword()) :: {:ok, term()} | {:error, Duffel.Error.t()}
Performs a GET request and unwraps the resource from the data
envelope. See get/3 and unwrap/1.
@spec list(t(), String.t(), keyword() | map()) :: {:ok, Duffel.Page.t()} | {:error, Duffel.Error.t()}
Performs a GET request against a list endpoint and wraps the result
in a Duffel.Page.
Like unwrap/1, a success response whose data is missing or is not a
list is an :unexpected_response error rather than an empty page.
Builds a client struct.
Raises ArgumentError if :access_token is missing.
The token is hidden when the struct is inspected, so it does not reach
logs or error trackers. Read client.access_token to get it back.
Examples
iex> client = Duffel.new(access_token: "duffel_test_abc")
iex> inspect(client) =~ "duffel_test_abc"
false
Performs a PATCH request, wrapping body in the data envelope.
Performs a PATCH request and unwraps the resource from the data
envelope. See patch/4 and unwrap/1.
Performs a POST request, wrapping body in the data envelope the
Duffel API expects.
Every POST carries an Idempotency-Key header. One is generated
unless you pass your own. Pass idempotency_key: nil to send no key at
all.
Duffel's API documentation does not describe how it treats this header, so do not count on it to collapse two identical bookings. Supply your own key when the caller may retry the same logical operation across processes or deploys, and check whether the resource already exists before retrying a create yourself.
Options
:params- query string parameters:idempotency_key- value for theIdempotency-Keyheader. Defaults to a generated key;nilsends no header.
Performs a POST request and unwraps the resource from the data
envelope. See post/4 and unwrap/1.
Performs a PUT request, wrapping body in the data envelope.
Performs a PUT request and unwraps the resource from the data
envelope. See put/4 and unwrap/1.
@spec stream(t(), String.t(), keyword() | map()) :: Enumerable.t()
Lazily streams every item from a paginated list endpoint, following
meta.after cursors until exhausted.
Raises Duffel.Error if any page request fails, or if Duffel hands back
the cursor it was just given, which would otherwise fetch the same page
forever.
@spec unwrap(response()) :: {:ok, term()} | {:error, Duffel.Error.t()}
Takes the resource out of the data envelope Duffel wraps it in.
A success response with no data key becomes an :unexpected_response
error rather than being handed back as if the envelope were the
resource. Errors pass through untouched.
get_data/3, post_data/4, patch_data/4 and put_data/4 do this for
you; reach for unwrap/1 directly only when you have built the request
some other way.
Examples
client |> get("/air/orders/ord_123") |> unwrap()
#=> {:ok, %{"id" => "ord_123", ...}}