Demandes Python - imprimer la requête http entière (brute)?

197

Lors de l'utilisation du requestsmodule , existe-t-il un moyen d'imprimer la requête HTTP brute?

Je ne veux pas seulement les en-têtes, je veux la ligne de demande, les en-têtes et l'impression de contenu. Est-il possible de voir ce qui est finalement construit à partir d'une requête HTTP?

huggie
la source
9
@RickyA il pose des questions sur le contenu de la demande, pas sur la réponse
goncalopp
2
C'est une bonne question. En regardant la source, il ne semble pas qu'il existe un moyen d'obtenir le contenu brut d'une demande préparée, et il n'est sérialisé que lorsqu'il est envoyé. Cela semble être une bonne fonctionnalité.
Tim Pierce
Eh bien, vous pouvez également démarrer wirehark et le voir de cette façon.
RickyA
@qwrrty, il serait difficile d'intégrer cela en tant que requestsfonctionnalité, car cela signifierait réécriture / contournement urllib3et httplib. Voir la trace de la pile ci
goncalopp
Cela a fonctionné pour moi - stackoverflow.com/questions/10588644/…
Ajay

Réponses:

213

Depuis la v1.2.3, les requêtes ont ajouté l'objet PreparedRequest. Selon la documentation "il contient les octets exacts qui seront envoyés au serveur".

On peut l'utiliser pour imprimer une jolie requête, comme ceci:

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)

qui produit:

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

a=1&b=2

Ensuite, vous pouvez envoyer la demande réelle avec ceci:

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

Ces liens sont vers la dernière documentation disponible, ils peuvent donc changer de contenu: Avancé - Demandes préparées et API - Classes de niveau inférieur

AntonioHerraizS
la source
2
C'est beaucoup plus robuste que ma méthode de correction de singe. La mise à niveau requestsest simple, donc je pense que cela devrait devenir la réponse acceptée
goncalopp
69
Si vous utilisez les méthodes simples response = requests.post(...)(ou requests.getou requests.put, etc.), vous pouvez réellement passer au PreparedResponsetravers response.request. Il peut économiser le travail de manipulation manuelle requests.Requestet requests.Session, si vous n'avez pas besoin d'accéder aux données http brutes avant de recevoir une réponse.
Gershom
2
Bonne réponse. Une chose que vous voudrez peut-être mettre à jour est que les sauts de ligne dans HTTP devraient être \ r \ n et pas seulement \ n.
ltc
3
qu'en est-il de la partie de la version du protocole HTTP juste après l'url? comme 'HTTP / 1.1'? cela ne se trouve pas lors de l'impression à l'aide de votre jolie imprimante.
Sajuuk
1
Mis à jour pour utiliser CRLF, car c'est ce que la RFC 2616 requiert, et cela pourrait être un problème pour les analyseurs très stricts
nimish
55
import requests
response = requests.post('http://httpbin.org/post', data={'key1':'value1'})
print(response.request.body)
print(response.request.headers)

J'utilise les requêtes version 2.18.4 et Python 3

Payman
la source
44

Remarque: cette réponse est obsolète. Les nouvelles versions de requests soutien obtenir le contenu de la demande directement, en tant que réponse de AntonioHerraizS documents .

Il n'est pas possible d'extraire le véritable contenu brut de la demande requests, car il ne traite que des objets de niveau supérieur, tels que les en- têtes et le type de méthode . requestsutilisations urllib3à envoyer des demandes, mais urllib3 aussi ne traite pas des données brutes - il utilise httplib. Voici une trace de pile représentative d'une demande:

-> 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)

À l'intérieur de la httplibmachine, nous pouvons voir HTTPConnection._send_requestindirectement les utilisations HTTPConnection._send_output, ce qui crée finalement la requête brute et le corps (s'il existe), et les utilise HTTPConnection.sendpour les envoyer séparément. sendatteint enfin la prise.

Puisqu'il n'y a pas d'hameçons pour faire ce que vous voulez, en dernier recours, vous pouvez réparer les singes httplibpour obtenir le contenu. C'est une solution fragile et vous devrez peut-être l'adapter en cas de httplibchangement. Si vous avez l'intention de distribuer des logiciels à l'aide de cette solution, vous voudrez peut-être envisager d'empaqueter httplibau lieu d'utiliser le système, ce qui est facile, car il s'agit d'un module python pur.

Hélas, sans plus tarder, la solution:

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")

ce qui donne la sortie:

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
la source
Salut goncalopp, si j'appelle la procédure patch_send () une deuxième fois (après une deuxième demande), alors il imprime les données deux fois (donc 2x fois la sortie comme vous l'avez montré ci-dessus)? Donc, si je faisais une 3ème demande, il l'imprimerait 3 fois et ainsi de suite ... Une idée comment obtenir une seule fois la sortie? Merci d'avance.
opstalj
@opstalj vous ne devez pas appeler patch_sendplusieurs fois, une seule fois, après l'importationhttplib
goncalopp
40

Une meilleure idée consiste à utiliser la bibliothèque requests_toolbelt, qui peut vider les demandes et les réponses sous forme de chaînes à imprimer sur la console. Il gère tous les cas délicats avec des fichiers et des encodages que la solution ci-dessus ne gère pas bien.

C'est aussi simple que cela:

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'))

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

Vous pouvez simplement l'installer en tapant:

pip install requests_toolbelt
Emil Stenström
la source
2
Cependant, cela ne semble pas vider la demande sans l'envoyer.
Dobes Vandermeer
1
dump_all ne semble pas fonctionner correctement car j'obtiens «TypeError: ne peut pas concaténer les objets« str »et« UUID »» de l'appel.
rtaft
@rtaft: veuillez signaler cela comme un bug dans leur dépôt github: github.com/sigmavirus24/requests-toolbelt/…
Emil Stenström
Il imprime le vidage avec les signes> et <, font-ils partie de la demande réelle?
Jay
1
@Jay Il semble qu'ils soient ajoutés à la demande / réponse réelle d'apparence ( github.com/requests/toolbelt/blob/master/requests_toolbelt/… ) et peuvent être spécifiés en passant request_prefix = b '{some_request_prefix}', response_prefix = b '{some_response_prefix}' à dump_all ( github.com/requests/toolbelt/blob/master/requests_toolbelt/… )
Christian Reall-Fluharty
7

Voici un code qui fait la même chose, mais avec des en-têtes de réponse:

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()

J'ai passé beaucoup de temps à chercher cela, donc je le laisse ici, si quelqu'un a besoin.

denself
la source
4

J'utilise la fonction suivante pour formater les demandes. C'est comme @AntonioHerraizS, sauf qu'il imprimera également les objets JSON dans le corps et étiqueter toutes les parties de la demande.

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

Et j'ai une fonction similaire pour formater la réponse:

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
la source
1

requestsprend en charge les soi-disant hooks d'événements (à partir de la version 2.23, il n'y a en fait que des hooksresponse ). Le hook peut être utilisé sur une demande pour imprimer les données complètes de la paire demande-réponse, y compris l'URL effective, les en-têtes et les corps, comme:

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})

L'exécuter 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>

Vous voudrez peut - être changer res.textde res.contentsi la réponse est binaire.

saaj
la source