Cómo enviar requests con la cookie de sesión mediante Guzzle y Symfony

Gerardo Fernández
Mar 25 · 5 min read

Hoy he preparado este artículo corto en el que os hablo de cómo podéis usar Guzzle para enviar requests (en vez de recurrir al por siempre famoso curl) pero que os recomiendo que leáis hasta el final (no, de verdad, no es autobombo) ya que el caso de uso que cuento os lo podéis encontrar en algún otro proyecto y así os ahorraréis las 4 horas que estuve yo pegándome hasta dar con la explicación y la solución.

¡Espero que os sirva!

Cómo enviar requests con Guzzle

Lo primero de todo será aprender a enviar requests con Guzzle. Si aún no conocéis esta librería veréis que es muy sencillo usarla (bastante más curl ) y que cuenta con algunas características muy interesantes como una especie de Promises al más puro estilo Javascript y la posibilidad de configurar middlewares para evitar tener que repetir determinados pasos en cada request que ejecutemos (por ejemplo, convertir en json la respuesta. Os animo a leer su documentación porque está bastante bien explicado, aunque es probable que prepare un artículo profundizando más en estos aspectos:

Veamos ahora el código para enviar una petición GET mediante esta librería:

// Create a client with a base URI$client = new GuzzleHttp\Client(['base_uri' =>      'https://foo.com/api/']);// Send a request to https://foo.com/api/test
$response = $client->request('GET', 'test');

Fácil y sencillo. Ahora, si queréis obtener el cuerpo de la respuesta, basta con:

$body = (string)$response->getBody();

y ya podréis manipularla como necesitéis.

La clase GuzzleApiRequest

Con el fin de encapsular las llamadas empleando esta librería, he creado la siguiente clase:

  • Como veréis, la clase extiende la interfaz ApiRequestInterface que he creado en mi proyecto con el fin de poder cambiar a otra librería si fuese necesario (ya sabéis, open-closed principle). Esta interfaz es tan sencilla como:
  • En la linea 22 se encuentra el constructor al cual le estoy pasando la url del backend así como el path de la API para crear el cliente de Guzzle con el parámetro base_uri apuntando a esa url.
  • Por otra parte, el método get replica el código de antes añadiendo la posibilidad de establecer los queryParams de nuestra request y convirtiendo en un json la respuesta obtenida de nuestra API.
  • Además, capturo la excepción RequestException de Guzzle para transformarla en una excepción propia (también llamada RequestException ) de modo que las clases que empleen la interfaz ApiRequestInterface trabajen con las mismas excepciones.
  • Y finalmente, también he implementado un método post para enviar ese tipo de peticiones. En él, compruebo si el array de files está vacío para enviar una petición normal o una multipart que incluya estos archivos. En la documentación podéis investigar un poco más sobre este tema:

Y con esto ya tendríamos nuestra clase funcionando para enviar peticiones post y get .

Enviando la cookie de sesión

Ahora suponed que necesitáis enviar la cookie de sesión con la petición, de modo que la API pueda autenticar la petición y recuperar el usuario que la realizó. El proceso es bastante sencillo (aunque esta vez de he decir que la documentación no fue todo lo clara que debería) y basta con modificar el constructor de la forma que véis a continuación:

Lo que he hecho es añadir dos nuevos parámetros:

  • RequestStack de modo que podamos recuperar la request actual y con ella las cookies.
  • cookieDomain para que en el objeto jar que pasaremos al cliente de Guzzle establezcamos el dominio al que pertenecen dichas cookies, algo muy útil en el caso de estemos trabajando con dominios y subdominios.

Finalmente, creo un objeto de la clase CookieJar con las cookies y su dominio y lo paso al constructor del cliente en el parámetro cookies . Con esto, las peticiones que enviemos empleando ese cliente ya irán con las cookies que le hayamos pasado.

Pero… y viene el problema…

Nota. Al constructor del cliente de Guzzle también podéis pasarle cookies => true de modo que creará él solo un JAR donde irá añadiendo las cookies que obtenga en cada petición:

Ah, que quieres abrir dos sesiones a la vez desde el mismo hilo…

Ahora suponed que tenéis la siguiente arquitectura:

  • Un backend montado en PHP (en este caso Symfony) que actúa como API
  • Un front montado en PHP (en este caso Symfony también) que hace peticiones a la API anterior mediante la clase que acabamos de escribir.
  • Un sistema compartido de sesión para el backend y el front (lo que aquí comento me ha pasado con las sesiones guardadas en el sistema de archivos pero creo que el problema será el mismo trabajando con Memcached , Redis o cualquier otro sistema, lo comprobaré la semana que viene)
  • Apache como webserver (no lo he probado con NGINX).
  • Al acceder a la URL /front-test del front, enviais con la clase anterior una petición a la url del backend /backend-test .

Igual os parece algo rebuscada pero creo que en proyectos de tamaño medio no es tan difícil toparse con algo así.

El caso es que, en el momento en que intentéis mandar una petición con las cookies desde el front, el back se quedará colgado. El motivo es el siguiente:

  • Apache abre una sesión para el front cuando se accede a /front-test
  • Ahora, en la URL front-est del front creamos la petición Guzzle hacia la URL backend-test con la cookie de session que obtenemos mediante la request .
  • Apache intenta abrir una segunda sesión para esa cookie pero no puede ya que está bloqueada por la primera.

Tras mucho investigar (pues no di a la primera con la secuencia de hechos que aquí os cuento) la causa del problema y un poquito de ayuda de Stackoverflow, la solución al problema es la siguiente:

  • Añadir el servicio Session de Symfony al constructor de nuestra clase GuzzleApiRequest
  • Cerrar la sesión empleando ese servicio justo antes de enviar la petición:
$this->session->save();
  • Reabrirla una vez que la petición se ha completado:
$this->session->start();

Con ello evitaremos intentar abrir dos sesiones simultáneas y todo funcionará como esperamos.

Por ejemplo, el método get quedaría del siguiente modo:

Puede parecer algo ortodoxo pero he sido incapaz de dar con una solución mejor y por todo lo que he leído, no parece haber otra forma. No obstante, si se os ocurre otra forma de resolver este problema me encantaría leerla.

Gracias a…

Y para terminar os dejo las dos respuestas de stackoverflow que me orientaron hacia la dirección correcta:

https://stackoverflow.com/questions/27657393/guzzle-hangs-apache-when-passing-phpsessid-session-cookie/44003679#44003679

https://stackoverflow.com/questions/44863237/session-write-close-symfony-2-and-3

Gerardo Fernández

Written by

Mejor amigo de Simba y desarrollador 100% fullstack