Codificador/decodificador SOAP/XML para Elixir, construido sobre OTP
(xmerl para parsing). El transporte HTTP es opcional e intercambiable:
trae tu propio cliente (HTTPoison, Tesla, Req, Finch...) o usa el :httpc
integrado sin agregar ninguna dependencia.
def deps do
[
{:xml_ex, "~> 0.2.0"},
# opcionales — solo si quieres usarlos como transporte:
{:httpoison, "~> 2.0"},
{:tesla, "~> 1.7"},
{:finch, "~> 0.16"}
]
endLos elementos se representan como tuplas {nombre, atributos, hijos}.
encode/2 genera el envelope y los headers HTTP listos para enviar;
decode/1 parsea la respuesta y detecta SOAP Faults:
body = {"tns:ObtenerSaldo", %{}, [{"tns:cuenta", %{}, "12345"}]}
request =
XmlEx.encode(body,
namespaces: %{"tns" => "http://example.com/banco"},
soap_action: "http://example.com/banco/ObtenerSaldo"
)
#=> %{body: "<?xml ...>", headers: [{"Content-Type", ...}, {"SOAPAction", ...}]}
# Con HTTPoison
{:ok, resp} = HTTPoison.post(url, request.body, request.headers)
XmlEx.decode(resp.body)
# Con Tesla
{:ok, env} = Tesla.post(client, url, request.body, headers: request.headers)
XmlEx.decode(env.body)decode/1 regresa {:ok, mapa}, {:error, %XmlEx.Fault{}} si la
respuesta trae un SOAP Fault, o {:error, reason} si el XML es inválido.
Codifica, envía y decodifica en un paso. El transporte es un módulo que
implementa el behaviour XmlEx.Transport:
# default: :httpc (OTP), sin dependencias
XmlEx.call(url, body, soap_action: "...")
# por llamada...
XmlEx.call(url, body, transport: XmlEx.Transport.HTTPoison)
XmlEx.call(url, body, transport: XmlEx.Transport.Tesla, tesla_client: client)
XmlEx.call(url, body, transport: XmlEx.Transport.Finch, finch_name: MiApp.Finch)
# ...o global, en config/config.exs
config :xml_ex, transport: XmlEx.Transport.HTTPoisonLos adapters de HTTPoison, Tesla y Finch solo se compilan si el paquete
correspondiente está entre tus dependencias. Para usar otro cliente,
implementa el behaviour (un solo callback, post/4):
defmodule MiApp.ReqTransport do
@behaviour XmlEx.Transport
@impl true
def post(url, body, headers, _opts) do
case Req.post(url, body: body, headers: headers) do
{:ok, resp} -> {:ok, %{status: resp.status, body: resp.body}}
{:error, reason} -> {:error, reason}
end
end
endLos transportes integrados heredan la configuración TLS del cliente HTTP subyacente, que por defecto no verifica el certificado del servidor: la conexión va cifrada pero el servidor no se autentica. Esto es cómodo para pruebas locales (servidores planos o con certificado self-signed).
En producción sí conviene activar la verificación con
XmlEx.TLS.secure_options/1:
# transporte default (:httpc)
XmlEx.call(url, body, ssl: XmlEx.TLS.secure_options())
# HTTPoison
XmlEx.call(url, body,
transport: XmlEx.Transport.HTTPoison,
httpoison_options: [ssl: XmlEx.TLS.secure_options()]
)
# Tesla (adapter :httpc por defecto)
XmlEx.call(url, body, transport: XmlEx.Transport.Tesla, ssl: XmlEx.TLS.secure_options())Con Tesla la opción :ssl apunta al adapter :httpc por defecto; si traes
otro adapter (hackney, Mint, Finch) configura TLS en tu propio
:tesla_client.
Para pruebas locales puedes omitir la opción :ssl o pasar
XmlEx.TLS.secure_options(verify: :verify_none).
Sin pooling, cada request abre una conexión nueva (y un handshake TLS nuevo). El pooling es opcional: para pruebas locales no hace falta, pero en producción bajo carga conviene reutilizar conexiones.
-
Finch (recomendado para pool nativo): el pool vive en la instancia que arrancas en tu árbol de supervisión. Una instancia simple sirve para pruebas; configura
pools:para producción.# application.ex — instancia con pool y TLS verificado children = [ {Finch, name: MiApp.Finch, pools: %{ :default => [size: 25, conn_opts: [transport_opts: XmlEx.TLS.secure_options()]] }} ] XmlEx.call(url, body, transport: XmlEx.Transport.Finch, finch_name: MiApp.Finch)
-
HTTPoison/hackney: usa un pool con nombre vía
:httpoison_options.# arranca el pool una vez (p. ej. en application.ex) :hackney_pool.start_pool(:xml_ex, timeout: 15_000, max_connections: 25) XmlEx.call(url, body, transport: XmlEx.Transport.HTTPoison, httpoison_options: [hackney: [pool: :xml_ex]] )
-
:httpc(default): reutiliza conexiones por profile cuando el servidor permite keep-alive; ajustable con las opcionesmax_sessions/max_keep_alive_lengthdel perfil de:httpc.
Algunos Listeners SOAP en Java (JAX-WS) se cuelgan ante el ClientHello de TLS 1.3 que OTP negocia por defecto. El síntoma es un connect colgado (no un recv lento). Solución: forzar TLS 1.2.
# combina con la verificación del certificado si la usas
ssl = XmlEx.TLS.secure_options(versions: [:"tlsv1.2"])
XmlEx.call(url, body, ssl: ssl) # :httpc
XmlEx.call(url, body, transport: XmlEx.Transport.HTTPoison,
httpoison_options: [ssl: ssl]) # HTTPoisonComo el cuelgue es en la conexión, conviene un timeout de conexión
separado del de recepción. En XmlEx.Client (:httpc) están separados:
XmlEx.call(url, body, connect_timeout: 5_000, timeout: 30_000)En HTTPoison, su opción :timeout es el timeout de conexión y :recv_timeout
el de recepción:
XmlEx.call(url, body,
transport: XmlEx.Transport.HTTPoison,
httpoison_options: [timeout: 5_000, recv_timeout: 30_000]
){:ok, parsed} = XmlEx.parse("<a><b>1</b><b>2</b><c>x</c></a>")
#=> {:ok, %{"a" => %{"b" => ["1", "2"], "c" => "x"}}}
XmlEx.Parser.dig(parsed, ["a", "c"])
#=> "x"| Módulo | Responsabilidad |
|---|---|
XmlEx |
Fachada: encode/2, decode/1, call/3, build/2, parse/1 |
XmlEx.Builder |
Genera XML a partir de tuplas Elixir |
XmlEx.Envelope |
Envelopes SOAP 1.1 / 1.2 |
XmlEx.Parser |
XML → mapas (vía xmerl) |
XmlEx.Fault |
Extracción de SOAP Faults (1.1 y 1.2) |
XmlEx.Transport |
Behaviour del transporte HTTP |
XmlEx.TLS |
Opciones TLS seguras (verificación del certificado) |
XmlEx.Client |
Transporte default vía :httpc (OTP) |
XmlEx.Transport.HTTPoison |
Adapter para HTTPoison (opcional) |
XmlEx.Transport.Tesla |
Adapter para Tesla (opcional) |
XmlEx.Transport.Finch |
Adapter para Finch con pool nativo (opcional) |
mix testIncluye tests de integración que levantan un servidor HTTP local y ejercitan los tres transportes de verdad.