Solicitudes de Python: ¿imprimir toda la solicitud http (sin formato)?

197

Al usar el requestsmódulo , ¿hay alguna forma de imprimir la solicitud HTTP sin procesar?

No quiero solo los encabezados, quiero la línea de solicitud, los encabezados y la impresión de contenido. ¿Es posible ver qué se construye a partir de la solicitud HTTP?

huggie
fuente
9
@RickyA pregunta por el contenido de la solicitud, no por la respuesta
goncalopp
2
Buena pregunta. Al mirar la fuente, no parece que haya ninguna forma de obtener el contenido sin procesar de una solicitud preparada, y solo se serializa cuando se envía. Parece que sería una buena característica.
Tim Pierce
Bueno, también podrías comenzar a Wirehark y verlo de esa manera.
RickyA
@qwrrty sería difícil integrar esto como una requestscaracterística, ya que significaría reescribir / omitir urllib3y httplib. Vea el seguimiento de la pila a continuación
goncalopp
Esto funcionó para mí - stackoverflow.com/questions/10588644/…
Ajay

Respuestas:

213

Desde v1.2.3 Requests agregó el objeto PreparedRequest. Según la documentación "contiene los bytes exactos que se enviarán al servidor".

Uno puede usar esto para imprimir una solicitud, así:

import requests

req = requests.Request('POST','http://stackoverflow.com',headers={'X-Custom':'Test'},data='a=1&b=2')
prepared = req.prepare()

def pretty_print_POST(req):
    """
    At this point it is completely built and ready
    to be fired; it is "prepared".

    However pay attention at the formatting used in 
    this function because it is programmed to be pretty 
    printed and may differ from the actual request.
    """
    print('{}\n{}\r\n{}\r\n\r\n{}'.format(
        '-----------START-----------',
        req.method + ' ' + req.url,
        '\r\n'.join('{}: {}'.format(k, v) for k, v in req.headers.items()),
        req.body,
    ))

pretty_print_POST(prepared)

que produce:

-----------START-----------
POST http://stackoverflow.com/
Content-Length: 7
X-Custom: Test

a=1&b=2

Luego puede enviar la solicitud real con esto:

s = requests.Session()
s.send(prepared)

Estos enlaces son a la última documentación disponible, por lo que pueden cambiar en contenido: Avanzado - Solicitudes preparadas y API - Clases de nivel inferior

AntonioHerraizS
fuente
2
Esto es mucho más robusto que mi método de parches de mono. La actualización requestses sencilla, así que creo que esta debería ser la respuesta aceptada
goncalopp
69
Si utiliza la sencilla response = requests.post(...)(o requests.get, o requests.putlos métodos, etc.), en realidad se puede obtener el PreparedResponsemedio response.request. Puede guardar el trabajo de manipulación manual requests.Requesty requests.Session, si no necesita acceder a los datos http sin procesar antes de recibir una respuesta.
Gershom
2
Buena respuesta. Sin embargo, una cosa que puede actualizar es que los saltos de línea en HTTP deberían ser \ r \ n no solo \ n.
ltc
3
¿Qué pasa con la parte de la versión del protocolo HTTP justo después de la url? como 'HTTP / 1.1'? eso no se encuentra al imprimir con su bonita impresora.
Sajuuk
1
Se actualizó para usar CRLF, ya que eso es lo que requiere RFC 2616, y podría ser un problema para analizadores muy estrictos
nimish el
55
import requests
response = requests.post('http://httpbin.org/post', data={'key1':'value1'})
print(response.request.body)
print(response.request.headers)

Estoy usando solicitudes versión 2.18.4 y Python 3

Payman
fuente
44

Nota: esta respuesta está desactualizada. Las versiones más recientes de requests apoyo para conseguir el contenido solicitud directamente, como la respuesta de AntonioHerraizS documentos .

No es posible obtener el verdadero contenido sin procesar de la solicitud requests, ya que solo trata con objetos de nivel superior, como encabezados y tipo de método . requestsusos urllib3para enviar peticiones, pero urllib3 también no se ocupa de los datos en bruto - que utiliza httplib. Aquí hay un seguimiento representativo de la pila de una solicitud:

-> r= requests.get("http://google.com")
  /usr/local/lib/python2.7/dist-packages/requests/api.py(55)get()
-> return request('get', url, **kwargs)
  /usr/local/lib/python2.7/dist-packages/requests/api.py(44)request()
-> return session.request(method=method, url=url, **kwargs)
  /usr/local/lib/python2.7/dist-packages/requests/sessions.py(382)request()
-> resp = self.send(prep, **send_kwargs)
  /usr/local/lib/python2.7/dist-packages/requests/sessions.py(485)send()
-> r = adapter.send(request, **kwargs)
  /usr/local/lib/python2.7/dist-packages/requests/adapters.py(324)send()
-> timeout=timeout
  /usr/local/lib/python2.7/dist-packages/requests/packages/urllib3/connectionpool.py(478)urlopen()
-> body=body, headers=headers)
  /usr/local/lib/python2.7/dist-packages/requests/packages/urllib3/connectionpool.py(285)_make_request()
-> conn.request(method, url, **httplib_request_kw)
  /usr/lib/python2.7/httplib.py(958)request()
-> self._send_request(method, url, body, headers)

Dentro de la httplibmaquinaria, podemos ver HTTPConnection._send_requestusos indirectos HTTPConnection._send_output, que finalmente crean la solicitud sin procesar y el cuerpo (si existe), y los utiliza HTTPConnection.sendpara enviarlos por separado. sendFinalmente llega al zócalo.

Como no hay ganchos para hacer lo que quieres, como último recurso, puedes usar parches httplibpara obtener el contenido. Es una solución frágil, y es posible que deba adaptarla si httplibse cambia. Si tiene la intención de distribuir software usando esta solución, puede considerar el empaquetado en httpliblugar de usar el sistema, lo cual es fácil, ya que es un módulo de Python puro.

Por desgracia, sin más preámbulos, la solución:

import requests
import httplib

def patch_send():
    old_send= httplib.HTTPConnection.send
    def new_send( self, data ):
        print data
        return old_send(self, data) #return is not necessary, but never hurts, in case the library is changed
    httplib.HTTPConnection.send= new_send

patch_send()
requests.get("http://www.python.org")

que produce la salida:

GET / HTTP/1.1
Host: www.python.org
Accept-Encoding: gzip, deflate, compress
Accept: */*
User-Agent: python-requests/2.1.0 CPython/2.7.3 Linux/3.2.0-23-generic-pae
goncalopp
fuente
Hola, goncalopp, si llamo al procedimiento patch_send () una segunda vez (después de una segunda solicitud), ¿imprime los datos dos veces (por lo que 2 veces la salida como se muestra arriba)? Entonces, si hiciera una tercera solicitud, la imprimiría 3 veces y así sucesivamente ... ¿Alguna idea de cómo obtener solo la salida una vez? Gracias por adelantado.
opstalj
@opstalj no debe llamar patch_sendvarias veces, solo una vez, después de importarhttplib
goncalopp
40

Una idea aún mejor es utilizar la biblioteca request_toolbelt, que puede volcar tanto las solicitudes como las respuestas como cadenas para que pueda imprimir en la consola. Maneja todos los casos difíciles con archivos y codificaciones que la solución anterior no maneja bien.

Es tan fácil como esto:

import requests
from requests_toolbelt.utils import dump

resp = requests.get('https://httpbin.org/redirect/5')
data = dump.dump_all(resp)
print(data.decode('utf-8'))

Fuente: https://toolbelt.readthedocs.org/en/latest/dumputils.html

Simplemente puede instalarlo escribiendo:

pip install requests_toolbelt
Emil Stenström
fuente
2
Sin embargo, esto no parece volcar la solicitud sin enviarla.
Dobes Vandermeer
1
dump_all no parece funcionar correctamente ya que obtengo "TypeError: no se pueden concatenar los objetos 'str' y 'UUID' de la llamada.
rtaft
@rtaft: Informe esto como un error en su repositorio de github
Emil Stenström
Imprime el volcado con signos> y <, ¿son parte de la solicitud real?
Jay
1
@ Jay Parece que están antepuestos a la solicitud / respuesta real para la apariencia ( github.com/requests/toolbelt/blob/master/requests_toolbelt/… ) y pueden especificarse pasando request_prefix = b '{some_request_prefix}', response_prefix = b '{some_response_prefix}' a dump_all ( github.com/requests/toolbelt/blob/master/requests_toolbelt/… )
Christian Reall-Fluharty
7

Aquí hay un código, que hace lo mismo, pero con encabezados de respuesta:

import socket
def patch_requests():
    old_readline = socket._fileobject.readline
    if not hasattr(old_readline, 'patched'):
        def new_readline(self, size=-1):
            res = old_readline(self, size)
            print res,
            return res
        new_readline.patched = True
        socket._fileobject.readline = new_readline
patch_requests()

Pasé mucho tiempo buscando esto, así que lo dejo aquí, si alguien lo necesita.

denso
fuente
4

Yo uso la siguiente función para formatear solicitudes. Es como @AntonioHerraizS, excepto que también imprimirá bonitos objetos JSON en el cuerpo y etiquetará todas las partes de la solicitud.

format_json = functools.partial(json.dumps, indent=2, sort_keys=True)
indent = functools.partial(textwrap.indent, prefix='  ')

def format_prepared_request(req):
    """Pretty-format 'requests.PreparedRequest'

    Example:
        res = requests.post(...)
        print(format_prepared_request(res.request))

        req = requests.Request(...)
        req = req.prepare()
        print(format_prepared_request(res.request))
    """
    headers = '\n'.join(f'{k}: {v}' for k, v in req.headers.items())
    content_type = req.headers.get('Content-Type', '')
    if 'application/json' in content_type:
        try:
            body = format_json(json.loads(req.body))
        except json.JSONDecodeError:
            body = req.body
    else:
        body = req.body
    s = textwrap.dedent("""
    REQUEST
    =======
    endpoint: {method} {url}
    headers:
    {headers}
    body:
    {body}
    =======
    """).strip()
    s = s.format(
        method=req.method,
        url=req.url,
        headers=indent(headers),
        body=indent(body),
    )
    return s

Y tengo una función similar para formatear la respuesta:

def format_response(resp):
    """Pretty-format 'requests.Response'"""
    headers = '\n'.join(f'{k}: {v}' for k, v in resp.headers.items())
    content_type = resp.headers.get('Content-Type', '')
    if 'application/json' in content_type:
        try:
            body = format_json(resp.json())
        except json.JSONDecodeError:
            body = resp.text
    else:
        body = resp.text
    s = textwrap.dedent("""
    RESPONSE
    ========
    status_code: {status_code}
    headers:
    {headers}
    body:
    {body}
    ========
    """).strip()

    s = s.format(
        status_code=resp.status_code,
        headers=indent(headers),
        body=indent(body),
    )
    return s
Ben
fuente
1

requestsadmite los llamados ganchos de eventos (a partir de 2.23 en realidad solo hay responseganchos). El enlace se puede usar en una solicitud para imprimir los datos completos del par solicitud-respuesta, incluyendo URL, encabezados y cuerpos efectivos, como:

import textwrap
import requests

def print_roundtrip(response, *args, **kwargs):
    format_headers = lambda d: '\n'.join(f'{k}: {v}' for k, v in d.items())
    print(textwrap.dedent('''
        ---------------- request ----------------
        {req.method} {req.url}
        {reqhdrs}

        {req.body}
        ---------------- response ----------------
        {res.status_code} {res.reason} {res.url}
        {reshdrs}

        {res.text}
    ''').format(
        req=response.request, 
        res=response, 
        reqhdrs=format_headers(response.request.headers), 
        reshdrs=format_headers(response.headers), 
    ))

requests.get('https://httpbin.org/', hooks={'response': print_roundtrip})

Ejecutarlo imprime:

---------------- request ----------------
GET https://httpbin.org/
User-Agent: python-requests/2.23.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive

None
---------------- response ----------------
200 OK https://httpbin.org/
Date: Thu, 14 May 2020 17:16:13 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 9593
Connection: keep-alive
Server: gunicorn/19.9.0
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

<!DOCTYPE html>
<html lang="en">
...
</html>

Es posible que desee cambiar res.texta res.contentsi la respuesta es binaria.

saaj
fuente