Solicitações Python - imprime a solicitação http inteira (bruta)?

197

Enquanto estiver usando o requestsmódulo , há alguma maneira de imprimir a solicitação HTTP não processada?

Não quero apenas os cabeçalhos, quero a linha de solicitação, os cabeçalhos e a impressão do conteúdo. É possível ver o que finalmente é construído a partir da solicitação HTTP?

huggie
fonte
9
@RickyA ele está perguntando sobre o conteúdo do pedido, não a resposta
goncalopp
2
Esta é uma boa pergunta. Observando a fonte, parece que não há como obter o conteúdo bruto de uma solicitação preparada, e ela é serializada apenas quando é enviada. Parece que seria uma boa característica.
Tim Pierce
Bem, você também pode iniciar o wireshark e vê-lo dessa maneira.
RickyA
@qwrrty seria difícil integrar isso como um requestsrecurso, pois isso significaria reescrever / ignorar urllib3e httplib. Veja o rastreamento da pilha abaixo
goncalopp
Isso funcionou para mim - stackoverflow.com/questions/10588644/…
Ajay

Respostas:

213

Desde a v1.2.3 Requests, foi adicionado o objeto PreparedRequest. Conforme a documentação "contém os bytes exatos que serão enviados ao servidor".

Pode-se usar isso para imprimir uma solicitação, assim:

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 produz:

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

a=1&b=2

Em seguida, você pode enviar a solicitação real com isso:

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

Esses links são para a documentação mais recente disponível, portanto, eles podem mudar de conteúdo: Avançado - Solicitações preparadas e API - Classes de nível inferior

AntonioHerraizS
fonte
2
Isso é muito mais robusto do que o meu método de correção de macacos. A atualização requestsé simples, então eu acho que isso deve tornar-se a resposta aceita
goncalopp
68
Se você usar os métodos simples response = requests.post(...)(ou requests.getou requests.putetc), poderá realmente obter o PreparedResponsemáximo response.request. Ele pode salvar o trabalho de manipulação manual requests.Requeste requests.Session, se você não precisar acessar os dados http brutos antes de receber uma resposta.
Gershom
2
Boa resposta. Uma coisa que você pode querer atualizar é que as quebras de linha no HTTP devem ser \ r \ n e não apenas \ n.
LTC
3
e a parte da versão do protocolo HTTP logo após o URL? gosta de 'HTTP / 1.1'? isso não é encontrado ao imprimir usando sua impressora bonita.
Sajuuk 29/05
1
Atualizados para usar CRLF, já que é o RFC 2616 exige, e que poderia ser um problema para os analisadores muito rigorosas
Nimish
55
import requests
response = requests.post('http://httpbin.org/post', data={'key1':'value1'})
print(response.request.body)
print(response.request.headers)

Estou usando os pedidos versão 2.18.4 e Python 3

Payman
fonte
44

Nota: esta resposta está desatualizada. Versões mais recentes do requests suporte recebendo o conteúdo pedido directamente, como resposta de AntonioHerraizS documentos .

Não é possível obter o verdadeiro conteúdo bruto da solicitação requests, pois ela lida apenas com objetos de nível superior, como cabeçalhos e tipo de método . requestsusa urllib3para enviar solicitações, mas urllib3 também não lida com dados brutos - ele usa httplib. Aqui está um rastreamento de pilha representativo de uma solicitação:

-> 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 da httplibmaquinaria, podemos ver HTTPConnection._send_requestos usos indiretos HTTPConnection._send_output, que finalmente criam a solicitação e o corpo brutos (se existir) e os HTTPConnection.sendenviam separadamente. sendfinalmente chega ao soquete.

Como não há ganchos para fazer o que você deseja, como último recurso, você pode httplibusar o patch para obter o conteúdo. É uma solução frágil, e pode ser necessário adaptá-lo se httplibfor alterado. Se você pretende distribuir software usando esta solução, considere a possibilidade de empacotar em httplibvez de usar o sistema, o que é fácil, pois é um módulo python puro.

Infelizmente, sem mais delongas, a solução:

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 produz a saída:

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
fonte
Oi goncalopp, se eu chamar o procedimento patch_send () uma segunda vez (após uma segunda solicitação), ele imprimirá os dados duas vezes (então 2x vezes a saída como você mostrou acima)? Então, se eu fizesse uma terceira solicitação, ela seria impressa 3x vezes e assim por diante ... Alguma idéia de como obter apenas a saída uma vez? Desde já, obrigado.
opstalj
@opstalj você não deve chamar patch_sendvárias vezes, apenas uma vez, depois de importarhttplib
goncalopp
40

Uma idéia ainda melhor é usar a biblioteca orders_toolbelt, que pode despejar solicitações e respostas como seqüências de caracteres para você imprimir no console. Ele lida com todos os casos complicados com arquivos e codificações que a solução acima não lida bem.

É tão fácil quanto isto:

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

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

Você pode simplesmente instalá-lo digitando:

pip install requests_toolbelt
Emil Stenström
fonte
2
Isso não parece despejar a solicitação sem enviá-la.
Dobes Vandermeer
1
O dump_all não parece funcionar corretamente quando recebo "TypeError: não é possível concatenar os objetos 'str' e 'UUID'" da chamada.
rtaft
@rtaft: Por favor, relate isso como um bug no repositório do github: github.com/sigmavirus24/requests-toolbelt/…
Emil Stenström
Ele imprime o despejo com sinais> e <, eles fazem parte da solicitação real?
22418 Jay
1
@Jay Parece que eles foram anexados à solicitação / resposta real para aparência ( github.com/requests/toolbelt/blob/master/requests_toolbelt/… ) e podem ser especificados passando request_prefix = b '{some_request_prefix}', response_prefix = b '{some_response_prefix}' para dump_all ( github.com/requests/toolbelt/blob/master/requests_toolbelt/… )
Christian Reall-Fluharty
7

Aqui está um código, que faz o mesmo, mas com cabeçalhos de resposta:

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

Passei muito tempo procurando por isso, então vou deixar aqui, se alguém precisar.

denself
fonte
4

Eu uso a seguinte função para formatar solicitações. É como o @AntonioHerraizS, exceto que também imprime objetos JSON no corpo e rotula todas as partes da solicitação.

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

E eu tenho uma função semelhante para formatar a resposta:

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
fonte
1

requestssuporta os chamados ganchos de eventos (a partir de 2.23, na verdade, existem apenas responseganchos). O gancho pode ser usado em uma solicitação para imprimir dados completos do par de solicitação-resposta, incluindo URL, cabeçalhos e corpos efetivos, 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})

Executá-lo 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>

Você pode mudar res.textpara res.contentse a resposta for binária.

saaj
fonte