Skip to content

Commit

Permalink
Remove threaded HTTP server
Browse files Browse the repository at this point in the history
I built it for Requests, but they deleted their threaded test since it didn't
really work very well.  The threaded server seems to cause some strange
problems with HTTP chunking, so I'll just remove it since nobody is using it (I
hope)
  • Loading branch information
kevin1024 committed Nov 22, 2015
1 parent 370891d commit ee0ff02
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 49 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ tox

## Changelog

* 0.2.0:
* Remove threaded HTTP server. I built it for Requests, but they deleted
their threaded test since it didn't really work very well. The threaded
server seems to cause some strange problems with HTTP chunking, so I'll
just remove it since nobody is using it (I hope)
* 0.1.1:
* Fix weird hang with SSL on pypy (again)
* 0.1.0:
Expand Down
88 changes: 56 additions & 32 deletions pytest_httpbin/serve.py
Original file line number Diff line number Diff line change
@@ -1,71 +1,94 @@
import os
import pytest_httpbin
import threading
import ssl
from werkzeug.serving import ThreadedWSGIServer, load_ssl_context, WSGIRequestHandler
from wsgiref.simple_server import WSGIServer, make_server, WSGIRequestHandler
from wsgiref.handlers import SimpleHandler
from six.moves.urllib.parse import urljoin


CERT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'certs')


class ServerHandler(SimpleHandler):

server_software = 'Pytest-HTTPBIN/0.1.0'
http_version = '1.1'

def cleanup_headers(self):
SimpleHandler.cleanup_headers(self)
self.headers['Connection'] = 'Close'

def close(self):
try:
self.request_handler.log_request(
self.status.split(' ', 1)[0], self.bytes_sent
)
finally:
SimpleHandler.close(self)


class Handler(WSGIRequestHandler):
server_version = 'pytest-httpbin/' + pytest_httpbin.__version__

def make_environ(self):
def handle(self):
"""Handle a single HTTP request"""

self.raw_requestline = self.rfile.readline()
if not self.parse_request(): # An error code has been sent, just exit
return

handler = ServerHandler(
self.rfile, self.wfile, self.get_stderr(), self.get_environ()
)
handler.request_handler = self # backpointer for logging
handler.run(self.server.get_app())

def get_environ(self):
"""
werkzeug server adds content-type text/plain to everything, this
wsgiref simple server adds content-type text/plain to everything, this
removes it if it's not actually in the headers.
"""
# Note: Can't use super since this is an oldstyle class in python 2.x
environ = super(Handler, self).make_environ().copy()
environ = WSGIRequestHandler.get_environ(self).copy()
if self.headers.get('content-type') is None:
del environ['CONTENT_TYPE']
return environ

class ThreadedWSGIServerWithSSLTimeout(ThreadedWSGIServer):
"""
This whole subclass exists just to set the ssl timeout before wrapping the
socket. That's because on pypy, if there's an SSL failure opening the
connection, it will hang forever.
"""

def __init__(self, *args, **kwargs):
self.protocol = kwargs.pop('protocol')
super(ThreadedWSGIServerWithSSLTimeout, self).__init__(*args, **kwargs)
class SecureWSGIServer(WSGIServer):

def finish_request(self, request, client_address):
"""
Negotiates SSL and then mimics BaseServer behavior.
"""
if self.protocol == 'https':
request.settimeout(1.0)
ssock = ssl.wrap_socket(
request,
keyfile=os.path.join(CERT_DIR, 'key.pem'),
certfile=os.path.join(CERT_DIR, 'cert.pem'),
server_side=True
)
self.RequestHandlerClass(ssock, client_address, self)
else:
self.RequestHandlerClass(request, client_address, self)
request.settimeout(1.0)
ssock = ssl.wrap_socket(
request,
keyfile=os.path.join(CERT_DIR, 'key.pem'),
certfile=os.path.join(CERT_DIR, 'cert.pem'),
server_side=True
)
self.RequestHandlerClass(ssock, client_address, self)
# WSGIRequestHandler seems to close the socket for us.
# Thanks, WSGIRequestHandler!!


class Server(threading.Thread):
"""
HTTP server running a WSGI application in its own thread.
"""

def __init__(self, host='127.0.0.1', port=0, application=None, protocol='http', **kwargs):
def __init__(self, host='127.0.0.1', port=0, application=None, **kwargs):
self.app = application
self._server = ThreadedWSGIServerWithSSLTimeout(
self._server = make_server(
host,
port,
self.app,
handler=Handler,
protocol=protocol,
handler_class=Handler,
**kwargs
)
self.host = self._server.server_address[0]
self.port = self._server.server_address[1]
self.protocol = protocol
self.protocol = 'http'

super(Server, self).__init__(
name=self.__class__,
Expand All @@ -91,5 +114,6 @@ def join(self, url, allow_fragments=True):

class SecureServer(Server):
def __init__(self, host='127.0.0.1', port=0, application=None, **kwargs):
super(SecureServer, self).__init__(host, port, application, protocol='https', **kwargs)
kwargs['server_class'] = SecureWSGIServer
super(SecureServer, self).__init__(host, port, application, **kwargs)
self.protocol = 'https'
2 changes: 1 addition & 1 deletion pytest_httpbin/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '0.1.1'
__version__ = '0.2.0'
22 changes: 6 additions & 16 deletions tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,6 @@
import pytest
from util import get_raw_http_response

try:
from multiprocessing.pool import ThreadPool
except ImportError:
ThreadPool = None


def test_content_type_header_not_automatically_added(httpbin):
"""
Expand All @@ -35,17 +30,12 @@ def test_unicode_data(httpbin):
assert resp.json()['data'] == u'оживлённым'



def test_server_should_handle_concurrent_connections(httpbin):
url = httpbin + '/get'
session = requests.Session()

def do_request(i):
r = session.get(url)
if ThreadPool is not None:
pool = ThreadPool(processes=50)
pool.map(do_request, range(100))

def test_server_should_be_http_1_1(httpbin):
"""
The server should speak HTTP/1.1 since we live in the future, see issue #6
"""
resp = get_raw_http_response(httpbin.host, httpbin.port, '/get')
assert resp.startswith(b'HTTP/1.1')

def test_dont_crash_on_certificate_problems(httpbin_secure):
with pytest.raises(Exception):
Expand Down

0 comments on commit ee0ff02

Please sign in to comment.