343

While using the requests module, is there any way to print the raw HTTP request?

I don't want just the headers, I want the request line, headers, and content printout. Is it possible to see what ultimately is constructed from HTTP request?

loopbackbee
  • 21,962
  • 10
  • 62
  • 97
huggie
  • 17,587
  • 27
  • 82
  • 139
  • 5
    That's a good question. From looking at the source, it doesn't seem like there is any way to obtain the raw content of a prepared request, and it's only serialized when it's sent. That seems like it would be a good feature. – Tim Pierce Dec 18 '13 at 12:55
  • Well, you could also start wireshark and see it that way. – RickyA Dec 18 '13 at 12:56
  • @qwrrty it would be difficult to integrate this as a `requests` feature, as it would mean rewritting/bypassing `urllib3` and `httplib`. See the stack trace below – loopbackbee Dec 18 '13 at 14:09
  • 2
    This worked for me - http://stackoverflow.com/questions/10588644/how-can-i-see-the-entire-http-request-thats-being-sent-by-my-python-application – Ajay May 27 '16 at 04:19
  • The logging answer linked by Ajay is really a helpful method. – rschwieb Apr 21 '23 at 20:35

9 Answers9

316

Since v1.2.3 Requests added the PreparedRequest object. As per the documentation "it contains the exact bytes that will be sent to the server".

One can use this to pretty print a request, like so:

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)

which produces:

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

a=1&b=2

Then you can send the actual request with this:

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

These links are to the latest documentation available, so they might change in content: Advanced - Prepared requests and API - Lower level classes

Calimo
  • 7,510
  • 4
  • 39
  • 61
AntonioHerraizS
  • 3,442
  • 1
  • 18
  • 12
  • This seems to [have been added on `2.0.0`](https://pypi.python.org/pypi/requests), though, not `1.2.3` – loopbackbee May 23 '14 at 07:55
  • @goncalopp I saw it mentioned in the documentation for 1.2.3, but I didn't look at the code. If you can confirm it was not present until 2.0.0 I'll change it to avoid confusion. – AntonioHerraizS May 23 '14 at 22:50
  • This is a great answer, but I think there's a typo: in the function definition, the argument should be 'prepared', 'not 'req'. – HaPsantran Jul 18 '14 at 18:57
  • 121
    If you use the simple `response = requests.post(...)` (or `requests.get` or `requests.put`, etc) methods, you can actually get the `PreparedResponse` through `response.request`. It can save the work of manually manipulating `requests.Request` and `requests.Session`, if you don't need to access the raw http data before you receive a response. – Gershom Maes Jun 24 '15 at 17:42
  • 5
    what about the HTTP protocol version part just after the url? like 'HTTP/1.1' ? that is not found when print out using your pretty printer. – Sajuuk May 29 '18 at 08:50
  • 1
    Updated to use CRLF, since that's what RFC 2616 requires, and it could be an issue for very strict parsers – nimish Aug 20 '19 at 18:32
  • Suggest replacing `req.body` with `str(req.body, 'UTF-8'),` so that multipart files will print prettily with all their boundaries and stuff. – Noumenon Nov 13 '19 at 03:24
  • Suggest replacing `req.body by `req.body if req.body else ''`. Otherwise, it prints `None` if there is nothing. It seems to me that it would be better to really print nothing at all. – Pierre Thibault Dec 13 '19 at 01:25
  • @Gershom Great tip, however, I just wanted to say I am incredibly grateful for this being illustrated as-is because it gives full control over the verb/headers/etc. For my use-case, there is no prefab solution within requests (HTTP PURGE) – Kamel Jun 24 '20 at 15:11
  • When trying to write this function output to file, a content was saved with double carriage return. \r should be omitted from the formatting line. See https://stackoverflow.com/a/4025988/1100913 for more details. – Andrey Oct 16 '20 at 15:27
  • This is wrong. `http://stackoverflow.com/` shouldn't be there. And is missing `HTTP/1.1`. This isn't valid HTTP request string. – gre_gor Sep 05 '22 at 22:12
  • 'Request' object has no attribute 'body'... – ntg Nov 16 '22 at 14:13
  • 1
    @ntg Well, then we all know you typed `pretty_print_POST(req)` rather than what is actually there (`pretty_print_POST(prepared)`. It takes a PreparedRequest, not a Request. – rschwieb Apr 21 '23 at 20:50
210
import requests

response = requests.post('http://httpbin.org/post', data={'key1': 'value1'})
print(response.request.url)
print(response.request.body)
print(response.request.headers)

Response objects have a .request property which is the PreparedRequest object that was sent.

Boris Verkhovskiy
  • 14,854
  • 11
  • 100
  • 103
Payman
  • 2,630
  • 1
  • 12
  • 18
  • Note that in case of redirections the `response.request` will actually contain the **last** `PreparedRequest` that has been sent. Try it with `https://httpbin.org/redirect/3`. To really dig out the original request from the response object you have to look in the `response.history`. – Jeyekomon Jun 23 '22 at 09:53
  • 1
    If the request throws an exception, this won't work. – gre_gor Sep 05 '22 at 22:14
66

An even better idea is to use the requests_toolbelt library, which can dump out both requests and responses as strings for you to print to the console. It handles all the tricky cases with files and encodings which the above solution does not handle well.

It's as easy as this:

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

You can simply install it by typing:

pip install requests_toolbelt
Emil Stenström
  • 13,329
  • 8
  • 53
  • 75
  • 3
    This doesn't seem to dump the request without sending it, though. – Dobes Vandermeer Mar 01 '16 at 02:10
  • 1
    dump_all does not appear to work properly as I get "TypeError: cannot concatenate 'str' and 'UUID' objects" from the call. – rtaft Jun 20 '16 at 18:51
  • @rtaft: Please report this as a bug in their github repository: https://github.com/sigmavirus24/requests-toolbelt/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen – Emil Stenström Jun 21 '16 at 09:19
  • It prints the dump with > and < signs, are they part of the actual request? – Jay Oct 22 '18 at 15:40
  • @DobesVandermeer there is an undocumented private function _dump_request_data that might do the trick https://github.com/requests/toolbelt/blob/master/requests_toolbelt/utils/dump.py#L57 – Christian Reall-Fluharty Sep 04 '19 at 18:06
  • 1
    @Jay It looks like they are prepended to the actual request/response for appearance (https://github.com/requests/toolbelt/blob/master/requests_toolbelt/utils/dump.py#L103) and can be specified by passing request_prefix=b'{some_request_prefix}', response_prefix=b'{some_response_prefix}' to dump_all (https://github.com/requests/toolbelt/blob/master/requests_toolbelt/utils/dump.py#L161) – Christian Reall-Fluharty Sep 04 '19 at 18:11
50

Note: this answer is for older versions of requests, when this functionality was missing. Newer versions support this natively (see other answers)

It's not possible to get the true raw content of the request out of requests, since it only deals with higher level objects, such as headers and method type. requests uses urllib3 to send requests, but urllib3 also doesn't deal with raw data - it uses httplib. Here's a representative stack trace of a request:

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

Inside the httplib machinery, we can see HTTPConnection._send_request indirectly uses HTTPConnection._send_output, which finally creates the raw request and body (if it exists), and uses HTTPConnection.send to send them separately. send finally reaches the socket.

Since there's no hooks for doing what you want, as a last resort you can monkey patch httplib to get the content. It's a fragile solution, and you may need to adapt it if httplib is changed. If you intend to distribute software using this solution, you may want to consider packaging httplib instead of using the system's, which is easy, since it's a pure python module.

Alas, without further ado, the 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")

which yields the output:

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
loopbackbee
  • 21,962
  • 10
  • 62
  • 97
  • Hi goncalopp, if I call the patch_send() procedure a 2nd time (after a 2nd request), then it prints the data twice (so 2x times the output like you have shown above) ? So, if I would do a 3rd request, it would print it 3x times and so on... Any idea how to get only the output once ? Thanks in advance. – opstalj Apr 28 '15 at 14:01
  • 1
    @opstalj you shouldn't call `patch_send` multiple times, only once, after importing `httplib` – loopbackbee Apr 29 '15 at 18:40
  • BTW, how did you get the stacktrace? Is it done by tracing the code or is there a trick? – huggie Dec 20 '20 at 14:39
  • 1
    @huggie no trick, just patience, manual stepping and reading the files – loopbackbee Dec 21 '20 at 18:08
  • For python 3, replace httplib with http.client – Brett Elliot Jan 25 '23 at 18:04
  • @loopbackbee, i like this solution for peeking at what urls are being called under the hood so that i can mock them with requests_mocker later. Would be cool if you had a `patch_receive` to peek at the responses too! – Brett Elliot Jan 25 '23 at 18:06
22

requests supports so called event hooks (as of 2.23 there's actually only response hook). The hook can be used on a request to print full request-response pair's data, including effective URL, headers and bodies, like:

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

Running it prints:

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

You may want to change res.text to res.content if the response is binary.

saaj
  • 23,253
  • 3
  • 104
  • 105
8

Here is a code, which makes the same, but with response headers:

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

I spent a lot of time searching for this, so I'm leaving it here, if someone needs.

denself
  • 106
  • 1
  • 2
5

A fork of @AntonioHerraizS answer (HTTP version missing as stated in comments)


Use this code to get a string representing the raw HTTP packet without sending it:

import requests


def get_raw_request(request):
    request = request.prepare() if isinstance(request, requests.Request) else request
    headers = '\r\n'.join(f'{k}: {v}' for k, v in request.headers.items())
    body = '' if request.body is None else request.body.decode() if isinstance(request.body, bytes) else request.body
    return f'{request.method} {request.path_url} HTTP/1.1\r\n{headers}\r\n\r\n{body}'


headers = {'User-Agent': 'Test'}
request = requests.Request('POST', 'https://stackoverflow.com', headers=headers, json={"hello": "world"})
raw_request = get_raw_request(request)
print(raw_request)

Result:

POST / HTTP/1.1
User-Agent: Test
Content-Length: 18
Content-Type: application/json

{"hello": "world"}

Can also print the request in the response object

r = requests.get('https://stackoverflow.com')
raw_request = get_raw_request(r.request)
print(raw_request)
Jossef Harush Kadouri
  • 32,361
  • 10
  • 130
  • 129
3

I use the following function to format requests. It's like @AntonioHerraizS except it will pretty-print JSON objects in the body as well, and it labels all parts of the request.

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

And I have a similar function to format the response:

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
  • 5,952
  • 4
  • 33
  • 44
1

test_print.py content:

import logging
import pytest
import requests
from requests_toolbelt.utils import dump


def print_raw_http(response):
    data = dump.dump_all(response, request_prefix=b'', response_prefix=b'')
    return '\n' * 2 + data.decode('utf-8')

@pytest.fixture
def logger():
    log = logging.getLogger()
    log.addHandler(logging.StreamHandler())
    log.setLevel(logging.DEBUG)
    return log

def test_print_response(logger):
    session = requests.Session()
    response = session.get('http://127.0.0.1:5000/')
    assert response.status_code == 300, logger.warning(print_raw_http(response))

hello.py content:

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'

Run:

 $ python -m flask hello.py
 $ python -m pytest test_print.py

Stdout:

------------------------------ Captured log call ------------------------------
DEBUG    urllib3.connectionpool:connectionpool.py:225 Starting new HTTP connection (1): 127.0.0.1:5000
DEBUG    urllib3.connectionpool:connectionpool.py:437 http://127.0.0.1:5000 "GET / HTTP/1.1" 200 13
WARNING  root:test_print_raw_response.py:25 

GET / HTTP/1.1
Host: 127.0.0.1:5000
User-Agent: python-requests/2.23.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive


HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 13
Server: Werkzeug/1.0.1 Python/3.6.8
Date: Thu, 24 Sep 2020 21:00:54 GMT

Hello, World!
klapshin
  • 761
  • 8
  • 14