Un servicio Express para invocación SOAP paralela en menos de 25 líneas de código

Visión general

Supongamos que existe un servicio que tiene las siguientes características:

  1. Expone un punto final REST que recibe una lista de solicitudes.
  2. En paralelo, invoca un servicio SOAP, una vez por elemento en la lista de solicitudes.
  3. Devuelve el resultado convertido de XML a JSON.

El código fuente de ese servicio podría verse así usando Node.js, Express y la Guía de estilo de JavaScript de Airbnb:

'use strict'; const { soap } = require('strong-soap'); const expressApp = require('express')(); const bodyParser = require('body-parser'); const url = '//www.dneonline.com/calculator.asmx?WSDL'; const clientPromise = new Promise((resolve, reject) => ( soap.createClient(url, {}, (err, client) => err ? reject(err) : resolve(client)) )); expressApp.use(bodyParser.json()) .post('/parallel-soap-invoke', (req, res) => (clientPromise.then(client => ({ client, requests: req.body })) .then(invokeOperations) .then(results => res.status(200).send(results)) .catch(({ message: error }) => res.status(500).send({ error })) )) .listen(3000, () => console.log('Waiting for incoming requests.')); const invokeOperations = ({ client, requests }) => (Promise.all(requests.map(request => ( new Promise((resolve, reject) => client.Add(request, (err, result) => ( err ? reject(err) : resolve(result)) )) ))));

Solicitud de muestra:

POST /parallel-soap-invoke [ { "intA": 1, "intB": 2 }, { "intA": 3, "intB": 4 }, { "intA": 5, "intB": 6 } ]

Respuesta de muestra:

HTTP/1.1 200 [ { "AddResult": 3 }, { "AddResult": 7 }, { "AddResult": 11 } ]

Las pruebas muestran que una sola solicitud directa al servicio SOAP usando SOAPUI toma ~ 430 ms (desde donde estoy ubicado, en Chile). Enviar tres solicitudes (como se muestra arriba) toma ~ 400 ms para las llamadas al servicio Express (que no sea la primera, que obtiene el WSDL y crea el cliente).

¿Por qué más solicitudes toman menos tiempo? Sobre todo porque el XML no está muy validado como en SOAP normal, por lo que si esta validación flexible no coincide con sus expectativas, debería considerar funciones o soluciones adicionales.

¿Se pregunta cómo se vería usando async/await? Aquí tienes (los resultados son los mismos):

'use strict'; const { soap } = require('strong-soap'); const expressApp = require('express')(); const bodyParser = require('body-parser'); const url = '//www.dneonline.com/calculator.asmx?WSDL'; const clientPromise = new Promise((resolve, reject) => ( soap.createClient(url, {}, (err, client) => err ? reject(err) : resolve(client)) )); expressApp.use(bodyParser.json()) .post('/parallel-soap-invoke', async (req, res) => { try { res.status(200).send(await invokeOperations(await clientPromise, req.body)); } catch ({message: error}) { res.status(500).send({ error }); } }) .listen(3000, () => console.log('Waiting for incoming requests.')); const invokeOperations = (client, requests) => (Promise.all(requests.map(request => ( new Promise((resolve, reject) => client.Add(request, (err, result) => ( err ? reject(err) : resolve(result)) )) ))));

La siguiente imagen proporciona un concepto de cómo funciona el código:

Este artículo tiene como objetivo mostrar la simplicidad de usar JavaScript para tareas en el mundo empresarial, como invocar servicios SOAP. Si está familiarizado con JavaScript, esto es básicamente solo Promise.allun par de devoluciones de llamada prometidas en un punto final Express. Puedes ir directamente a la sección 4 ( Bonus track ) si crees que podría ser útil para ti.

Si está fuera del mundo de JavaScript, creo que 24 líneas de código para las tres características que mencioné al principio son un buen negocio. Ahora entraré en detalles.

1. La sección Express

Comencemos con el código relacionado con Express, un marco de aplicación web Node.js. mínimo y flexible. Es bastante simple y puedes encontrarlo en cualquier lugar, así que te daré una descripción resumida.

'use strict'; // Express framework. const express = require('express'); // Creates an Express application. const app = express(); /** * Creates a GET (which is defined by the method invoked on 'app') endpoint, * having 'parallel-soap-invoke' as entry point. * Each time a GET request arrives at '/parallel-soap-invoke', the function passed * as the second parameter from app.get will be invoked. * The signature is fixed: the request and response objects. */ app.get('/parallel-soap-invoke', (_, res) => { // HTTP status of the response is set first and then the result to be sent. res.status(200).send('Hello!'); }); // Starts 'app' and sends a message when it's ready. app.listen(3000, () => console.log('Waiting for incoming requests.'));

Resultado:

GET /parallel-soap-invoke HTTP/1.1 200 Hello!

Ahora necesitaremos manejar un objeto enviado a través de POST. Express body-parserpermite un fácil acceso al cuerpo de la solicitud:

 'use strict'; const expressApp = require('express')(); // Compressing two lines into one. const bodyParser = require('body-parser'); // Several parsers for HTTP requests. expressApp.use(bodyParser.json()) // States that 'expressApp' will use JSON parser. // Since each Express method returns the updated object, methods can be chained. .post('/parallel-soap-invoke', (req, res) => { /** * As an example, the same request body will be sent as response with * a different HTTP status code. */ res.status(202).send(req.body); // req.body will have the parsed object }) .listen(3000, () => console.log('Waiting for incoming requests.'));
POST /parallel-soap-invoke content-type: application/json [ { "intA": 1, "intB": 2 }, { "intA": 3, "intB": 4 }, { "intA": 5, "intB": 6 } ] HTTP/1.1 202 [ { "intA": 1, "intB": 2 }, { "intA": 3, "intB": 4 }, { "intA": 5, "intB": 6 } ] 

En resumen: configure la aplicación Express y, tan pronto como tenga el resultado, envíelo a través de resy listo.

2. La sección SOAP

Esto tendrá algunos pasos más que la sección anterior. La idea principal es que, para hacer invocaciones SOAP en paralelo, usaré Promise.all. Para poder usar Promise.all, la invocación a los servicios SOAP debe manejarse dentro de una Promesa, lo que no es el caso strong-soap. Esta sección le mostrará cómo convertir las devoluciones de llamada regulares de strong-soapen Promesas y luego poner una Promise.allencima de eso.

El siguiente código utilizará el ejemplo más básico de strong-soapla documentación de. Lo simplificaré un poco y usaré el mismo WSDL que hemos estado viendo (no usé el mismo WSDL indicado en strong-soapla documentación, ya que ese WSDL ya no funciona):

'use strict'; // The SOAP client library. var { soap } = require('strong-soap'); // WSDL we'll be using through the article. var url = '//www.dneonline.com/calculator.asmx?WSDL'; // Hardcoded request var requestArgs = { "intA": 1, "intB": 2, }; // Creates the client which is returned in the callback. soap.createClient(url, {}, (_, client) => ( // Callback delivers the result of the SOAP invokation. client.Add(requestArgs, (_, result) => ( console.log(`Result: ${"\n" + JSON.stringify(result)}`) )) ));
$ node index.js Result: {"AddResult":3}

Convertiré esto en Promesas y revisaré todas las devoluciones de llamada, una por una, por el bien del ejemplo. De esa manera, el proceso de traducción será muy claro para usted:

'use strict'; var { soap } = require('strong-soap'); var url = '//www.dneonline.com/calculator.asmx?WSDL'; var requestArgs = { "intA": 1, "intB": 2, }; /** * A function that will return a Promise which will return the SOAP client. * The Promise receives as parameter a function having two functions as parameters: * resolve & reject. * So, as soon as you got a result, call resolve with the result, * or call reject with some error otherwise. */ const createClient = () => (new Promise((resolve, reject) => ( // Same call as before, but I'm naming the error parameter since I'll use it. soap.createClient(url, {}, (err, client) => ( /** * Did any error happen? Let's call reject and send the error. * No? OK, let's call resolve sending the result. */ err ? reject(err) : resolve(client) )))) ); /** * The above function is invoked. * The Promise could have been inlined here, but it's more understandable this way. */ createClient().then( /** * If at runtime resolve is invoked, the value sent through resolve * will be passed as parameter for this function. */ client => (client.Add(requestArgs, (_, result) => ( console.log(`Result: ${"\n" + JSON.stringify(result)}`) ))), // Same as above, but in this case reject was called at runtime. err => console.log(err), );

Llamar node index.jsobtiene el mismo resultado que antes. Siguiente devolución de llamada:

'use strict'; var { soap } = require('strong-soap'); var url = '//www.dneonline.com/calculator.asmx?WSDL'; var requestArgs = { "intA": 1, "intB": 2, }; const createClient = () => (new Promise((resolve, reject) => ( soap.createClient(url, {}, (err, client) => ( err ? reject(err) : resolve(client) )))) ); /** * Same as before: do everything you need to do; once you have a result, * resolve it, or reject some error otherwise. * invokeOperation will replace the first function of .then from the former example, * so the signatures must match. */ const invokeOperation = client => (new Promise((resolve, reject) => ( client.Add(requestArgs, (err, result) => ( err ? reject(err) : resolve(result) )) ))); /** * .then also returns a Promise, having as result the value resolved or rejected * by the functions that were passed as parameters to it. In this case, the second .then * will receive the value resolved/rejected by invokeOperation. */ createClient().then( invokeOperation, err => console.log(err), ).then( result => console.log(`Result: ${"\n" + JSON.stringify(result)}`), err => console.log(err), );

node index.js? Siempre lo mismo. Envuelva esas promesas en una función, a fin de preparar el código para llamarlo dentro del punto final Express. También simplifica un poco el manejo de errores:

'use strict'; var { soap } = require('strong-soap'); var url = '//www.dneonline.com/calculator.asmx?WSDL'; var requestArgs = { "intA": 1, "intB": 2, }; const createClient = () => (new Promise((resolve, reject) => ( soap.createClient(url, {}, (err, client) => ( err ? reject(err) : resolve(client) )))) ); const invokeOperation = client => (new Promise((resolve, reject) => ( client.Add(requestArgs, (err, result) => ( err ? reject(err) : resolve(result) )) ))); const processRequest = () => createClient().then(invokeOperation); /** * .catch() will handle any reject not handled by a .then. In this case, * it will handle any reject called by createClient or invokeOperation. */ processRequest().then(result => console.log(`Result: ${"\n" + JSON.stringify(result)}`)) .catch(({ message }) => console.log(message));

Apuesto a que puedes adivinar el resultado de node index.js.

¿Qué sucede si se realizan varias llamadas posteriores? Lo averiguaremos con el siguiente código:

'use strict'; var { soap } = require('strong-soap'); var url = '//www.dneonline.com/calculator.asmx?WSDL'; var requestArgs = { "intA": 1, "intB": 2, }; const createClient = () => (new Promise((resolve, reject) => ( soap.createClient(url, {}, (err, client) => { if (err) { reject(err); } else { // A message is displayed each time a client is created. console.log('A new client is being created.'); resolve(client); } }))) ); const invokeOperation = client => (new Promise((resolve, reject) => ( client.Add(requestArgs, (err, result) => ( err ? reject(err) : resolve(result) )) ))); const processRequest = () => createClient().then(invokeOperation) processRequest().then(result => console.log(`Result: ${"\n" + JSON.stringify(result)}`)) .catch(({ message }) => console.log(message)); processRequest().then(result => console.log(`Result: ${"\n" + JSON.stringify(result)}`)) .catch(({ message }) => console.log(message)); processRequest().then(result => console.log(`Result: ${"\n" + JSON.stringify(result)}`)) .catch(({ message }) => console.log(message));
$ node index.js A new client is being created. A new client is being created. Result: {"AddResult":3} A new client is being created. Result: {"AddResult":3} Result: {"AddResult":3}

No es bueno, ya que se están creando varios clientes. Idealmente, el cliente debe almacenarse en caché y reutilizarse. Hay dos formas principales de lograr esto:

  1. Puede crear una variable fuera de Promise y almacenar en caché el cliente tan pronto como lo tenga (justo antes de resolverlo). Llamemos a esto cachedClient. Pero, en ese caso, tendría que lidiar manualmente con las llamadas createClient()realizadas entre la primera vez que se llama y antes de que se resuelva el primer cliente. Tendría que inspeccionar si cachedClientes el valor esperado, o tendría que verificar si la Promesa está resuelta o no, o tendría que poner algún tipo de emisor de eventos para saber cuándo cachedClientestá listo. La primera vez que escribí código para esto, utilicé este enfoque y terminé viviendo con el hecho de que cada llamada realizada antes de la primera createClient().resolvesobrescritura cachedClient. Si el problema no está tan claro, avíseme y escribiré el código y los ejemplos.
  2. Promises have a very cool feature (see MDN documentation, “Return value” section): if you call .then() on a resolved/rejected Promise, it will return the very same value that was resolved/rejected, without processing again. In fact, very technically, it will be the very same object reference.

The second approach is much simpler to implement, so the related code is the following:

'use strict'; var { soap } = require('strong-soap'); var url = '//www.dneonline.com/calculator.asmx?WSDL'; var requestArgs = { "intA": 1, "intB": 2, }; // createClient function is removed. const clientPromise = (new Promise((resolve, reject) => ( soap.createClient(url, {}, (err, client) => { if (err) { reject(err); } else { console.log('A new client is being created.'); resolve(client); } }))) ); const invokeOperation = client => (new Promise((resolve, reject) => ( client.Add(requestArgs, (err, result) => ( err ? reject(err) : resolve(result) )) ))); // clientPromise is called instead getClient(). clientPromise.then(invokeOperation) .then(result => console.log(`Result: ${"\n" + JSON.stringify(result)}`)) .catch(({ message }) => console.log(message)); clientPromise.then(invokeOperation) .then(result => console.log(`Result: ${"\n" + JSON.stringify(result)}`)) .catch(({ message }) => console.log(message)); clientPromise.then(invokeOperation) .then(result => console.log(`Result: ${"\n" + JSON.stringify(result)}`)) .catch(({ message }) => console.log(message));
$ node index.js A new client is being created. Result: {"AddResult":3} Result: {"AddResult":3} Result: {"AddResult":3}

Finally for this section, let’s make the code handle several parallel calls. This will be easy:

  1. For handling several parallel calls, we’ll need Promise.all.
  2. Promise.all has a single parameter: an array of Promises. So we’ll be converting the list of requests into a list of Promises. The code currently converts a single request into a single Promise (invokeOperation), so the code just needs a .map to achieve this.
'use strict'; var { soap } = require('strong-soap'); var url = '//www.dneonline.com/calculator.asmx?WSDL'; // Hardcoded list of requests. var requestsArgs = [ { "intA": 1, "intB": 2, }, { "intA": 3, "intB": 4, }, { "intA": 5, "intB": 6, }, ]; const clientPromise = (new Promise((resolve, reject) => ( soap.createClient(url, {}, (err, client) => err ? reject(error) : resolve(client)) ))); // Promise.all on top of everything. const invokeOperation = client => (Promise.all( // For each request, a Promise is returned. requestsArgs.map(requestArgs => new Promise((resolve, reject) => ( // Everything remains the same here. client.Add(requestArgs, (err, result) => ( err ? reject(err) : resolve(result) )) ))) )); clientPromise.then(invokeOperation) .then(result => console.log(`Result: ${"\n" + JSON.stringify(result)}`)) .catch(({ message }) => console.log(message));
$ node index.js Result: [{"AddResult":3},{"AddResult":7},{"AddResult":11}]

3. Putting it all together

This is fairly easy — it’s just assembling the last code from each previous section:

'use strict'; const { soap } = require('strong-soap'); const expressApp = require('express')(); const bodyParser = require('body-parser'); const url = '//www.dneonline.com/calculator.asmx?WSDL'; const clientPromise = new Promise((resolve, reject) => ( soap.createClient(url, {}, (err, client) => err ? reject(err) : resolve(client)) )); expressApp.use(bodyParser.json()) .post('/parallel-soap-invoke', (req, res) => (clientPromise.then(invokeOperations) .then(results => res.status(200).send(results)) .catch(({ message: error }) => res.status(500).send({ error })) )) .listen(3000, () => console.log('Waiting for incoming requests.')); // Adding req.body instead of hardcoded requests. const invokeOperations = client => Promise.all(req.body.map(request => ( new Promise((resolve, reject) => client.Add(request, (err, result) => ( err ? reject(err) : resolve(result)) )) )));
POST /parallel-soap-invoke [ { "intA": 1, "intB": 2 }, { "intA": 3, "intB": 4 }, { "intA": 5, "intB": 6 } ] HTTP/1.1 500 { "error": "req is not defined" }

Hmmm… Not a good result, since I did not expect an error at all. The problem is that invokeOperations doesn’t have req in its scope. The first thought could be “Just add it to the signature.” But that’s not possible, as that signature matches the result from the previous Promise, and that promise doesn’t return req, it only returns client. But, what if we add an intermediate Promise whose only purpose is injecting this value?

'use strict'; const { soap } = require('strong-soap'); const expressApp = require('express')(); const bodyParser = require('body-parser'); const url = '//www.dneonline.com/calculator.asmx?WSDL'; const clientPromise = new Promise((resolve, reject) => ( soap.createClient(url, {}, (err, client) => err ? reject(err) : resolve(client)) )); expressApp.use(bodyParser.json()) .post('/parallel-soap-invoke', (req, res) => ( /** * After clientPromise.then, where client is received, a new Promise is * created, and that Promise will resolve an object having two properties: * client and requests. */ clientPromise.then(client => ({ client, requests: req.body })) .then(invokeOperations) .then(results => res.status(200).send(results)) .catch(({ message: error }) => res.status(500).send({ error })) )) .listen(3000, () => console.log('Waiting for incoming requests.')); /** * Since the shape of the object passed to invokeOperations changed, the signature has * to change to reflect the shape of the new object. */ const invokeOperations = ({ client, requests }) => Promise.all(requests.map(request => ( new Promise((resolve, reject) => client.Add(request, (err, result) => ( err ? reject(err) : resolve(result)) )) )));

The results are exactly the same as the ones at the summary.

4. Bonus track

A generic SOAP to JSON converter for parallel SOAP invoking. The code is familiar, based on what you saw in the former sections. How about that?

'use strict'; const { soap } = require('strong-soap'); const expressApp = require('express')(); const bodyParser = require('body-parser'); const clientPromises = new Map(); expressApp.use(bodyParser.json()) .post('/parallel-soap-invoke', ({ body: { wsdlUrl, operation, requests } }, res) => ( getClient(wsdlUrl).then(client => ({ client, operation, requests })) .then(invokeOperations) .then(results => res.status(200).send(results)) .catch(({ message: error }) => res.status(500).send({ error })) )) .listen(3000, () => console.log('Waiting for incoming requests.')); const getClient = wsdlUrl => clientPromises.get(wsdlUrl) || (clientPromises.set(wsdlUrl, new Promise((resolve, reject) => ( soap.createClient(wsdlUrl, {}, (err, client) => err ? reject(err) : resolve(client)) ))).get(wsdlUrl)); const invokeOperations = ({ client, operation, requests }) => (Promise.all(requests.map(request => ( new Promise((resolve, reject) => client[operation](request, (err, result) => ( err ? reject(err) : resolve(result)) )) ))));

First use example:

POST /parallel-soap-invoke content-type: application/json { "wsdlUrl": "//www.dneonline.com/calculator.asmx?WSDL", "operation": "Add", "requests": [ { "intA": 1, "intB": 2 }, { "intA": 3, "intB": 4 }, { "intA": 5, "intB": 6 } ] } HTTP/1.1 200 [ { "AddResult": 3 }, { "AddResult": 7 }, { "AddResult": 11 } ] 

Second use example:

POST /parallel-soap-invoke content-type: application/json { "wsdlUrl": "//ws.cdyne.com/ip2geo/ip2geo.asmx?wsdl", "operation": "ResolveIP", "requests": [ { "ipAddress": "8.8.8.8", "licenseKey": "" }, { "ipAddress": "8.8.4.4", "licenseKey": "" } ] } HTTP/1.1 200 [ { "ResolveIPResult": { "Country": "United States", "Latitude": 37.75101, "Longitude": -97.822, "AreaCode": "0", "HasDaylightSavings": false, "Certainty": 90, "CountryCode": "US" } }, { "ResolveIPResult": { "Country": "United States", "Latitude": 37.75101, "Longitude": -97.822, "AreaCode": "0", "HasDaylightSavings": false, "Certainty": 90, "CountryCode": "US" } } ]

Are you going through Digital Decoupling? In a JavaScript full-stack architecture on top of the old services, this artifact could help you encapsulate all SOAP services, extend them, and expose only JSON. You could even modify this code a bit to call several different SOAP services at the same time (that should be just an additional .map and .reduce, as I see it right now). Or you could encapsulate your enterprise’s WSDLs in a database and invoke them based on a code or some identifier. That would be just one or two additional promises to the chain.