Send request body as application/x-www-form-urlencoded.
Performs encoding of body from a Map such as %{"foo" => "bar"} into
URL-encoded data.
Performs decoding of the response into a map when urlencoded and content-type
is application/x-www-form-urlencoded, so "foo=bar" becomes
%{"foo" => "bar"}.
Examples
defmodule Myclient do
def client do
Tesla.client([
{Tesla.Middleware.FormUrlencoded,
encode: &Plug.Conn.Query.encode/1,
decode: &Plug.Conn.Query.decode/1}
])
end
end
client = Myclient.client()
Tesla.post(client, "/url", %{key: :value})Options
:decode- decoding function, defaults toURI.decode_query/1:encode- controls how the body is encoded. Accepts:- an arity-1 function that receives the body and returns a binary
(e.g.
&Plug.Conn.Query.encode/1or any custom encoder) :brackets— recursive bracket-notation encoder for nested maps and lists (seeencode: :bracketsbelow){:brackets, opts}— same encoder with sub-options. Currently supportsboolean_as: :string | :integer(seeencode: :bracketsbelow).- Defaults to
URI.encode_query/1when omitted.
- an arity-1 function that receives the body and returns a binary
(e.g.
Nested Maps
Natively, nested maps are not supported in the body, so
%{"foo" => %{"bar" => "baz"}} won't be encoded and raise an error.
Support for this specific case is obtained either by setting
encode: :brackets (see encode: :brackets below) or by
configuring the middleware to encode (and decode) with
Plug.Conn.Query:
defmodule Myclient do
def client do
Tesla.client([
{Tesla.Middleware.FormUrlencoded,
encode: &Plug.Conn.Query.encode/1,
decode: &Plug.Conn.Query.decode/1}
])
end
end
client = Myclient.client()
Tesla.post(client, "/url", %{key: %{nested: "value"}})encode: :brackets
Recursive indexed-bracket form encoder for nested maps and lists.
The output shape — key[0]=a&key[1]=b for arrays, a[b][c]=1 for
nested objects — is the de-facto convention used by Stripe, Rack,
PHP's http_build_query, qs (with arrayFormat: 'indices'), and
ASP.NET model binding. It is not defined by any RFC. The OpenAPI 3.0.4
and 3.1.1 specs cover the object case under deepObject
style but
explicitly state that "the representation of array or object
properties is not defined", which this encoder fills in with the
conventions above.
Percent-encoding of keys and values follows
application/x-www-form-urlencoded (WHATWG URL Standard / PHP's
default PHP_QUERY_RFC1738): spaces become +, reserved characters
become %XX.
client = Tesla.client([{Tesla.Middleware.FormUrlencoded, encode: :brackets}])
Tesla.post(client, "/url", %{
expand: ["objects"],
objects: %{customers: ["cus_123", "cus_456"]}
})
# body: "expand[0]=objects&objects[customers][0]=cus_123&objects[customers][1]=cus_456"Booleans (boolean_as)
Booleans default to the lowercase strings true / false, which is
the wire format required by Stripe's V2 API (see stripe-python PR
#1499) and used by
every official Stripe SDK. PHP's http_build_query would instead emit
1 for true and 0 for false; opt in to that behavior with
boolean_as: :integer:
# default: Stripe-compatible
client = Tesla.client([{Tesla.Middleware.FormUrlencoded, encode: :brackets}])
Tesla.post(client, "/url", %{active: true})
# body: "active=true"
# opt in to PHP http_build_query parity
client =
Tesla.client([{Tesla.Middleware.FormUrlencoded, encode: {:brackets, boolean_as: :integer}}])
Tesla.post(client, "/url", %{active: true})
# body: "active=1"With boolean_as: :integer, the encoder's output matches PHP
http_build_query byte-for-byte (after URL-decoding %5B/%5D to
literal [/]) across the verified test corpus. Use :string
(default) for Stripe and most modern APIs; use :integer only when
targeting a server that specifically requires the PHP-native form.
nil vs empty string
The encoder distinguishes "don't include this field" from "include the
field with an empty value". This matches the three-state semantics
exposed by Stripe's update endpoints (and PHP's http_build_query):
| Input value | Wire output | Typical API meaning on update |
|---|---|---|
nil | (nothing emitted) | Field is absent from the request — the server leaves the existing value untouched. |
"" (empty string) | key= | Field is present with an empty value — the server clears or unsets it. |
| any other value | key=value | Field is set to the given value. |
Use nil to mean "leave this field alone" and "" to mean "clear this
field". For example, on POST /v1/customers/cus_X:
# Leaves customer.metadata.foo untouched (field not in request):
%{metadata: %{plan: "pro"}}
# → metadata[plan]=pro
# Deletes the foo key from customer.metadata (sent with empty value):
%{metadata: %{foo: ""}}
# → metadata[foo]=Inside lists the same rule applies element-by-element: nil elements
are dropped while the index of remaining elements is preserved
([a, nil, b] → [0]=a&[2]=b, matching PHP http_build_query), and
"" elements are emitted as key[i]=.
Root containers
The root accepts any ordered name/value pair-sequence, which matches
the application/x-www-form-urlencoded wire-level data model:
Map— most common; iteration order is undefined.- Keyword list (
[a: 1, b: 2]) — preserves the order you give it. - List of 2-tuples (
[{"tag", "a"}, {"tag", "b"}]) — preserves order and allows duplicate keys and non-atom keys ([{"tag", "a"}, {"tag", "b"}]→tag=a&tag=b).
Anything else at the root (42, [1, 2, 3], [{:a, 1}, 2], a
struct) raises ArgumentError.
Nested containers
Inside the payload the shape is strict, no inference:
Mapis always an object →parent[key]=value.Listis always an array →parent[0]=…&parent[1]=….- A tuple appearing inside a map or list raises — use a map for object-shaped nesting.
- Structs raise
ArgumentError— convert them withMap.from_struct/1orto_string/1first.
Other behavior worth knowing
- Empty lists and empty maps emit nothing, which means the parent key
disappears entirely:
%{ids: []}→"", and%{user: %{tags: []}}→"". This matches PHPhttp_build_query. Send""to clear a field on Stripe-style update endpoints. - Map keys are not ordered; keyword lists and lists of 2-tuples preserve the order you give them.
- Decoding stays flat.
Plug.Conn.Query.decode/1will parse the bracket keys into nested maps, but indexed lists come back as maps keyed by string indices ("0","1", …), not as Elixir lists, so the round-trip is not symmetric.
Summary
Functions
Decode response body as querystring.
It is used by Tesla.Middleware.DecodeFormUrlencoded.
Encode response body as querystring.
It is used by Tesla.Middleware.EncodeFormUrlencoded.