From 2f35821c235a5e747b57b8b2c3d84154dbcbaea1 Mon Sep 17 00:00:00 2001 From: Ben Darnell Date: Mon, 5 Mar 2012 21:38:47 -0800 Subject: [PATCH] Avoid modifying the headers object passed in by the client. This fixes a problem in which the header object was reused across requests and behavior became inconsistent when following redirects. Closes #459. --- tornado/httputil.py | 9 ++++++++- tornado/simple_httpclient.py | 6 ++++-- tornado/test/simple_httpclient_test.py | 8 ++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/tornado/httputil.py b/tornado/httputil.py index c44b735c5..a1068224e 100644 --- a/tornado/httputil.py +++ b/tornado/httputil.py @@ -58,7 +58,14 @@ class HTTPHeaders(dict): dict.__init__(self) self._as_list = {} self._last_key = None - self.update(*args, **kwargs) + if (len(args) == 1 and len(kwargs) == 0 and + isinstance(args[0], HTTPHeaders)): + # Copy constructor + for k,v in args[0].get_all(): + self.add(k,v) + else: + # Dict-style initialization + self.update(*args, **kwargs) # new public methods diff --git a/tornado/simple_httpclient.py b/tornado/simple_httpclient.py index 755c63ae0..2fca25784 100644 --- a/tornado/simple_httpclient.py +++ b/tornado/simple_httpclient.py @@ -94,8 +94,10 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): def fetch(self, request, callback, **kwargs): if not isinstance(request, HTTPRequest): request = HTTPRequest(url=request, **kwargs) - if not isinstance(request.headers, HTTPHeaders): - request.headers = HTTPHeaders(request.headers) + # We're going to modify this (to add Host, Accept-Encoding, etc), + # so make sure we don't modify the caller's object. This is also + # where normal dicts get converted to HTTPHeaders objects. + request.headers = HTTPHeaders(request.headers) callback = stack_context.wrap(callback) self.queue.append((request, callback)) self._process_queue() diff --git a/tornado/test/simple_httpclient_test.py b/tornado/test/simple_httpclient_test.py index f2fc12d3b..bbfd57b1e 100644 --- a/tornado/test/simple_httpclient_test.py +++ b/tornado/test/simple_httpclient_test.py @@ -6,6 +6,7 @@ import logging import re import socket +from tornado.httputil import HTTPHeaders from tornado.ioloop import IOLoop from tornado.simple_httpclient import SimpleAsyncHTTPClient, _DEFAULT_CA_CERTS from tornado.test.httpclient_test import HTTPClientCommonTestCase, ChunkHandler, CountdownHandler, HelloWorldHandler @@ -179,6 +180,13 @@ class SimpleHTTPClientTestCase(AsyncHTTPTestCase, LogTrapTestCase): self.assertTrue(response.effective_url.endswith("/countdown/2")) self.assertTrue(response.headers["Location"].endswith("/countdown/1")) + def test_header_reuse(self): + # Apps may reuse a headers object if they are only passing in constant + # headers like user-agent. The header object should not be modified. + headers = HTTPHeaders({'User-Agent': 'Foo'}) + self.fetch("/hello", headers=headers) + self.assertEqual(list(headers.get_all()), [('User-Agent', 'Foo')]) + def test_303_redirect(self): response = self.fetch("/303_post", method="POST", body="blah") self.assertEqual(200, response.code) -- 2.47.2