linusdm

linusdm

Building a SOAP endpoint in Phoenix

I’m researching the possibility of reviving a project that makes use of ancient SOAP technology. Since hardware is involved, a switch to a newer technology stack is not option, unfortunately.

Has anybody been successful at serving SOAP requests somehow, in a Phoenix project? I see there are some SOAP client libraries on hex.pm, but no SOAP servers.

There was a prior discussion over here, but it concluded that it’s a no-go for Elixir. I hope to find some new evidence that it is possible.

I’m curious if this could be solved by accepting the HTTP requests as usual with Plug or in a Phoenix controller. But then call out to a lower level implementation (possibly C or C++) that does the legwork of parsing the request. Then act upon that request and formulate a response in plain Elixir. And then finally encode the Elixir response by passing it once more through the low-level library that generates the required XML that can be send back by the Plug connection.
I’ve seen libraries like Apache Axis and gSOAP that theoretically could do such magic. But I’m not sure what it would take to actually create this integration.

Other nudges toward better ideas are very much appreciated.

PS: the wsdl specification I’m interested in, if it is of any help, is located here and documented here. It’s a specification for distributing Daisy talking books (audiobooks).

Most Liked

Awlexus

Awlexus

Sorry for the late reply. We create a custom plug that handles the parsing of the incoming soap requests and some utilities for sending a reply.

Using this plug builds a model from the wsdl at compile time, parses and validates incoming requests, and; similar to a controller; executes a function with the same name.

# Example module
defmodule YourCoolSoapService.ServicePlug do
  # Usage
  # wsdl_file: path to your wsdl file. xsd files should be relative to the files too
  # prefix: this prefix will be 
  use SoapService.Soap.Plug, wsdl_file: "your/wsdl/file/here.wsdl", prefix: "abc"

  # Imagine you have an operation called playMusic with 
  # the request/response types playMusicRequest and playMusicReply
  def play_music(conn, abc_playMusicRequest() = params) do
    # play the music
    case play_the_music(params) do
      {:ok, result} -> soap_resp(conn, abc_playMusicResponse())
      {:error, error} -> client_fault(conn, "That was your fault")
    end
  end
end
# Generic soap error
defmodule SoapService.SoapFaultError do
  defexception [:detail, :message]
end

defmodule SoapService.Soap.Plug do
  defmacro __using__(config) do
    quote location: :keep do
      # extracts the records from the wsdl
      use Erlsom.Records, unquote(config)

      # Expose utilities
      import SoapService.Soap.Plug
      alias SoapService.SoapFaultError

      # extracts records for the wsdl terms
      for record <- [:wsdl, :"soap:detail", :"soap:Fault", :"soap:Body", :"soap:Header", :"soap:Envelope"] do
        record_name = Atom.to_string(record) |> String.replace(":", "") |> String.to_atom()
        Record.defrecord(record_name, record, Record.extract(record, from_lib: "detergent/include/detergent.hrl"))
      end

      def init(opts), do: opts

      def call(conn, _opts) do
        # read the body, parse it and validate it against the schema
        {:ok, body, conn} = Plug.Conn.read_body(conn)
        # erlsom_model defined at compiletime by using Erlsom.Records
        model = erlsom_model()

        case :erlsom.scan(body, model) do
          # Valid wsdl operation
          {:ok, result, _rest} ->
            soap_body = soapEnvelope(result, :Body)

            # Extract the requested operation
            case soapBody(soap_body, :choice) do
              [soap_op] ->
                action = record_to_action(soap_op)

                # Apply the matching function for the requested operation
                try do
                  apply(__MODULE__, action, [conn, soap_op])
                rescue
                  e in SoapFaultError ->
                    client_fault(conn, e.message, e.detail)

                  e ->
                    require Logger
                    Logger.error(Exception.format(:error, e, __STACKTRACE__))
                    client_fault(conn, e.message)
                end

              _ ->
                client_fault(conn, "No such operation")
            end

          {:error, error} ->
            client_fault(conn, error)
        end
      end

      # Helper function that wraps the response with the proper soap terms
      def soap_resp(conn, response) do
        envelope = soapEnvelope(Header: soapHeader(), Body: soapBody(choice: [response]))
        SoapService.Soap.Plug.soap_resp(conn, envelope, erlsom_model())
      end

      # Helper function to build non-raising error
      def client_fault(conn, message, detail \\ nil) do
        detail =
          if detail do
            soap_detail("#any": [detail])
          else
            :undefined
          end

        fault =
          soapFault(
            faultcode: {:qname, 'http://schemas.xmlsoap.org/soap/envelope/', 'Client', 'soap', ''},
            faultstring: to_charlist(message),
            detail: detail
          )

        soap_resp(conn, fault)
      end
    end
  end

  # Transform the result into a proper soap response and send it
  def soap_resp(conn, envelope, model) do
    {:ok, xml} = :erlsom.write(envelope, model)

    conn
    |> Plug.Conn.put_resp_content_type("text/xml")
    |> Plug.Conn.send_resp(200, :erlsom_ucs.to_utf8(xml))
    |> Plug.Conn.halt()
  end

  def record_to_action(tuple) do
    tuple
    |> elem(0)
    |> Atom.to_string()
    |> remove_prefix()
    |> Macro.underscore()
    |> String.to_existing_atom()
  end

  defp remove_prefix(string) do
    if new_string = do_remove_prefix(string) do
      new_string
    else
      string
    end
  end

  defp do_remove_prefix(""), do: nil
  defp do_remove_prefix(":" <> rest), do: rest
  defp do_remove_prefix(<<_, rest::binary>>), do: do_remove_prefix(rest)
end
Awlexus

Awlexus

If you have a wsdl file for the service, you might be able to use the plug from this library. soapex/lib/soapex/plugs/soap.ex at master · kim-company/soapex · GitHub. We are using this library with a wrapper around this plug to serve a production soap/wsdl api. I’d be happy to help if you have additional questions :smile:

dimitarvp

dimitarvp

I’d say if the clients (hardware devices that can’t have their software changed, I take it?) do not utilize a central repository of WSDL and SOAP registries and hundreds of schemas then you’re better off just constructing the XML yourself manually. Both Erlang and Elixir have numerous libraries for this but if you struggle with it, we can help.

Ultimately it all boils down to this: WSDL / SOAP are just producing and parsing XML according to certain XML Schema files. That’s it really. Don’t be threatened. No need to shell out to other programming languages unless time is super tight.

Where Next?

Popular in Discussions Top

Nvim
Anybody knows a comprehensive comparison of Django and Phoenix, thanks for the help. Where are they similar? Where do they differ the m...
New
gausby
I asked this very same question on twitter and got some interesting feedback, but I thought it would be a good question to ask here as we...
1207 39467 209
New
crabonature
I’m still quite new to Elixir. As I understand we got in Elixir “multi guards” as convention to simplify one large guard with or’s?: de...
New
tmbb
This is a post to discuss the new Phoenix LiveView functionality. From Chris’s talk, it appears that they generate all HTML on the serve...
342 18243 126
New
shishini
I think this twitter post and youtube video didn’t get as much attention as I hoped I am still new to Elixir, so can’t really judge ...
New
klo
Got a question about when to concat vs. prepending items to list then reversing to achieve appending. So i know lists boil down to [1 | ...
New
ben-pr-p
In general I’ve been sticking to this community style guide GitHub - christopheradams/elixir_style_guide: A community driven style guide ...
New
AstonJ
If so I (and hopefully others!) might have some tips for you :slight_smile: But first, please say which area you’re finding most challen...
New
AstonJ
Can you believe the first professionally published Elixir book was published just 8 years ago? Since then I think we’ve seen more books f...
New
Owens
Hello all, I am developing a new mobile app with Flutter frontend and Phoenix backend. The mobile app has real-time task management and c...
New

Other popular topics Top

sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 31013 112
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
Lily
In templates/appointment/index.html.eex: &lt;%= for appointment &lt;- @appointments do %&gt; &lt;tr&gt; &lt;td&gt;&lt;%= appoi...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement