##// END OF EJS Templates
hgweb: when constructing or adding to a wsgi environ dict, use native strs...
Augie Fackler -
r34513:482d6f6d default
parent child Browse files
Show More
@@ -1,152 +1,152 b''
1 1 # hgweb/request.py - An http request from either CGI or the standalone server.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import cgi
12 12 import errno
13 13 import socket
14 14
15 15 from .common import (
16 16 ErrorResponse,
17 17 HTTP_NOT_MODIFIED,
18 18 statusmessage,
19 19 )
20 20
21 21 from .. import (
22 22 util,
23 23 )
24 24
25 25 shortcuts = {
26 26 'cl': [('cmd', ['changelog']), ('rev', None)],
27 27 'sl': [('cmd', ['shortlog']), ('rev', None)],
28 28 'cs': [('cmd', ['changeset']), ('node', None)],
29 29 'f': [('cmd', ['file']), ('filenode', None)],
30 30 'fl': [('cmd', ['filelog']), ('filenode', None)],
31 31 'fd': [('cmd', ['filediff']), ('node', None)],
32 32 'fa': [('cmd', ['annotate']), ('filenode', None)],
33 33 'mf': [('cmd', ['manifest']), ('manifest', None)],
34 34 'ca': [('cmd', ['archive']), ('node', None)],
35 35 'tags': [('cmd', ['tags'])],
36 36 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
37 37 'static': [('cmd', ['static']), ('file', None)]
38 38 }
39 39
40 40 def normalize(form):
41 41 # first expand the shortcuts
42 42 for k in shortcuts.iterkeys():
43 43 if k in form:
44 44 for name, value in shortcuts[k]:
45 45 if value is None:
46 46 value = form[k]
47 47 form[name] = value
48 48 del form[k]
49 49 # And strip the values
50 50 for k, v in form.iteritems():
51 51 form[k] = [i.strip() for i in v]
52 52 return form
53 53
54 54 class wsgirequest(object):
55 55 """Higher-level API for a WSGI request.
56 56
57 57 WSGI applications are invoked with 2 arguments. They are used to
58 58 instantiate instances of this class, which provides higher-level APIs
59 59 for obtaining request parameters, writing HTTP output, etc.
60 60 """
61 61 def __init__(self, wsgienv, start_response):
62 version = wsgienv['wsgi.version']
62 version = wsgienv[r'wsgi.version']
63 63 if (version < (1, 0)) or (version >= (2, 0)):
64 64 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
65 65 % version)
66 self.inp = wsgienv['wsgi.input']
67 self.err = wsgienv['wsgi.errors']
68 self.threaded = wsgienv['wsgi.multithread']
69 self.multiprocess = wsgienv['wsgi.multiprocess']
70 self.run_once = wsgienv['wsgi.run_once']
66 self.inp = wsgienv[r'wsgi.input']
67 self.err = wsgienv[r'wsgi.errors']
68 self.threaded = wsgienv[r'wsgi.multithread']
69 self.multiprocess = wsgienv[r'wsgi.multiprocess']
70 self.run_once = wsgienv[r'wsgi.run_once']
71 71 self.env = wsgienv
72 72 self.form = normalize(cgi.parse(self.inp,
73 73 self.env,
74 74 keep_blank_values=1))
75 75 self._start_response = start_response
76 76 self.server_write = None
77 77 self.headers = []
78 78
79 79 def __iter__(self):
80 80 return iter([])
81 81
82 82 def read(self, count=-1):
83 83 return self.inp.read(count)
84 84
85 85 def drain(self):
86 86 '''need to read all data from request, httplib is half-duplex'''
87 87 length = int(self.env.get('CONTENT_LENGTH') or 0)
88 88 for s in util.filechunkiter(self.inp, limit=length):
89 89 pass
90 90
91 91 def respond(self, status, type, filename=None, body=None):
92 92 if self._start_response is not None:
93 93 self.headers.append(('Content-Type', type))
94 94 if filename:
95 95 filename = (filename.rpartition('/')[-1]
96 96 .replace('\\', '\\\\').replace('"', '\\"'))
97 97 self.headers.append(('Content-Disposition',
98 98 'inline; filename="%s"' % filename))
99 99 if body is not None:
100 100 self.headers.append(('Content-Length', str(len(body))))
101 101
102 102 for k, v in self.headers:
103 103 if not isinstance(v, str):
104 104 raise TypeError('header value must be string: %r' % (v,))
105 105
106 106 if isinstance(status, ErrorResponse):
107 107 self.headers.extend(status.headers)
108 108 if status.code == HTTP_NOT_MODIFIED:
109 109 # RFC 2616 Section 10.3.5: 304 Not Modified has cases where
110 110 # it MUST NOT include any headers other than these and no
111 111 # body
112 112 self.headers = [(k, v) for (k, v) in self.headers if
113 113 k in ('Date', 'ETag', 'Expires',
114 114 'Cache-Control', 'Vary')]
115 115 status = statusmessage(status.code, str(status))
116 116 elif status == 200:
117 117 status = '200 Script output follows'
118 118 elif isinstance(status, int):
119 119 status = statusmessage(status)
120 120
121 121 self.server_write = self._start_response(status, self.headers)
122 122 self._start_response = None
123 123 self.headers = []
124 124 if body is not None:
125 125 self.write(body)
126 126 self.server_write = None
127 127
128 128 def write(self, thing):
129 129 if thing:
130 130 try:
131 131 self.server_write(thing)
132 132 except socket.error as inst:
133 133 if inst[0] != errno.ECONNRESET:
134 134 raise
135 135
136 136 def writelines(self, lines):
137 137 for line in lines:
138 138 self.write(line)
139 139
140 140 def flush(self):
141 141 return None
142 142
143 143 def close(self):
144 144 return None
145 145
146 146 def wsgiapplication(app_maker):
147 147 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
148 148 can and should now be used as a WSGI application.'''
149 149 application = app_maker()
150 150 def run_wsgi(env, respond):
151 151 return application(env, respond)
152 152 return run_wsgi
@@ -1,334 +1,334 b''
1 1 # hgweb/server.py - The standalone hg web server.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import errno
12 12 import os
13 13 import socket
14 14 import sys
15 15 import traceback
16 16
17 17 from ..i18n import _
18 18
19 19 from .. import (
20 20 error,
21 21 pycompat,
22 22 util,
23 23 )
24 24
25 25 httpservermod = util.httpserver
26 26 socketserver = util.socketserver
27 27 urlerr = util.urlerr
28 28 urlreq = util.urlreq
29 29
30 30 from . import (
31 31 common,
32 32 )
33 33
34 34 def _splitURI(uri):
35 35 """Return path and query that has been split from uri
36 36
37 37 Just like CGI environment, the path is unquoted, the query is
38 38 not.
39 39 """
40 40 if '?' in uri:
41 41 path, query = uri.split('?', 1)
42 42 else:
43 43 path, query = uri, ''
44 44 return urlreq.unquote(path), query
45 45
46 46 class _error_logger(object):
47 47 def __init__(self, handler):
48 48 self.handler = handler
49 49 def flush(self):
50 50 pass
51 51 def write(self, str):
52 52 self.writelines(str.split('\n'))
53 53 def writelines(self, seq):
54 54 for msg in seq:
55 55 self.handler.log_error("HG error: %s", msg)
56 56
57 57 class _httprequesthandler(httpservermod.basehttprequesthandler):
58 58
59 59 url_scheme = 'http'
60 60
61 61 @staticmethod
62 62 def preparehttpserver(httpserver, ui):
63 63 """Prepare .socket of new HTTPServer instance"""
64 64
65 65 def __init__(self, *args, **kargs):
66 self.protocol_version = 'HTTP/1.1'
66 self.protocol_version = r'HTTP/1.1'
67 67 httpservermod.basehttprequesthandler.__init__(self, *args, **kargs)
68 68
69 69 def _log_any(self, fp, format, *args):
70 70 fp.write("%s - - [%s] %s\n" % (self.client_address[0],
71 71 self.log_date_time_string(),
72 72 format % args))
73 73 fp.flush()
74 74
75 75 def log_error(self, format, *args):
76 76 self._log_any(self.server.errorlog, format, *args)
77 77
78 78 def log_message(self, format, *args):
79 79 self._log_any(self.server.accesslog, format, *args)
80 80
81 81 def log_request(self, code='-', size='-'):
82 82 xheaders = []
83 83 if util.safehasattr(self, 'headers'):
84 84 xheaders = [h for h in self.headers.items()
85 85 if h[0].startswith('x-')]
86 86 self.log_message('"%s" %s %s%s',
87 87 self.requestline, str(code), str(size),
88 88 ''.join([' %s:%s' % h for h in sorted(xheaders)]))
89 89
90 90 def do_write(self):
91 91 try:
92 92 self.do_hgweb()
93 93 except socket.error as inst:
94 94 if inst[0] != errno.EPIPE:
95 95 raise
96 96
97 97 def do_POST(self):
98 98 try:
99 99 self.do_write()
100 100 except Exception:
101 101 self._start_response("500 Internal Server Error", [])
102 102 self._write("Internal Server Error")
103 103 self._done()
104 104 tb = "".join(traceback.format_exception(*sys.exc_info()))
105 105 self.log_error("Exception happened during processing "
106 106 "request '%s':\n%s", self.path, tb)
107 107
108 108 def do_GET(self):
109 109 self.do_POST()
110 110
111 111 def do_hgweb(self):
112 112 path, query = _splitURI(self.path)
113 113
114 114 env = {}
115 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
116 env['REQUEST_METHOD'] = self.command
117 env['SERVER_NAME'] = self.server.server_name
118 env['SERVER_PORT'] = str(self.server.server_port)
119 env['REQUEST_URI'] = self.path
120 env['SCRIPT_NAME'] = self.server.prefix
121 env['PATH_INFO'] = path[len(self.server.prefix):]
122 env['REMOTE_HOST'] = self.client_address[0]
123 env['REMOTE_ADDR'] = self.client_address[0]
115 env[r'GATEWAY_INTERFACE'] = r'CGI/1.1'
116 env[r'REQUEST_METHOD'] = self.command
117 env[r'SERVER_NAME'] = self.server.server_name
118 env[r'SERVER_PORT'] = str(self.server.server_port)
119 env[r'REQUEST_URI'] = self.path
120 env[r'SCRIPT_NAME'] = self.server.prefix
121 env[r'PATH_INFO'] = path[len(self.server.prefix):]
122 env[r'REMOTE_HOST'] = self.client_address[0]
123 env[r'REMOTE_ADDR'] = self.client_address[0]
124 124 if query:
125 env['QUERY_STRING'] = query
125 env[r'QUERY_STRING'] = query
126 126
127 127 if self.headers.typeheader is None:
128 env['CONTENT_TYPE'] = self.headers.type
128 env[r'CONTENT_TYPE'] = self.headers.type
129 129 else:
130 env['CONTENT_TYPE'] = self.headers.typeheader
130 env[r'CONTENT_TYPE'] = self.headers.typeheader
131 131 length = self.headers.getheader('content-length')
132 132 if length:
133 env['CONTENT_LENGTH'] = length
133 env[r'CONTENT_LENGTH'] = length
134 134 for header in [h for h in self.headers.keys()
135 135 if h not in ('content-type', 'content-length')]:
136 hkey = 'HTTP_' + header.replace('-', '_').upper()
137 hval = self.headers.getheader(header)
138 hval = hval.replace('\n', '').strip()
136 hkey = r'HTTP_' + header.replace(r'-', r'_').upper()
137 hval = self.headers.get(header)
138 hval = hval.replace(r'\n', r'').strip()
139 139 if hval:
140 140 env[hkey] = hval
141 env['SERVER_PROTOCOL'] = self.request_version
142 env['wsgi.version'] = (1, 0)
143 env['wsgi.url_scheme'] = self.url_scheme
144 if env.get('HTTP_EXPECT', '').lower() == '100-continue':
141 env[r'SERVER_PROTOCOL'] = self.request_version
142 env[r'wsgi.version'] = (1, 0)
143 env[r'wsgi.url_scheme'] = self.url_scheme
144 if env.get(r'HTTP_EXPECT', '').lower() == '100-continue':
145 145 self.rfile = common.continuereader(self.rfile, self.wfile.write)
146 146
147 env['wsgi.input'] = self.rfile
148 env['wsgi.errors'] = _error_logger(self)
149 env['wsgi.multithread'] = isinstance(self.server,
147 env[r'wsgi.input'] = self.rfile
148 env[r'wsgi.errors'] = _error_logger(self)
149 env[r'wsgi.multithread'] = isinstance(self.server,
150 150 socketserver.ThreadingMixIn)
151 env['wsgi.multiprocess'] = isinstance(self.server,
151 env[r'wsgi.multiprocess'] = isinstance(self.server,
152 152 socketserver.ForkingMixIn)
153 env['wsgi.run_once'] = 0
153 env[r'wsgi.run_once'] = 0
154 154
155 155 self.saved_status = None
156 156 self.saved_headers = []
157 157 self.sent_headers = False
158 158 self.length = None
159 159 self._chunked = None
160 160 for chunk in self.server.application(env, self._start_response):
161 161 self._write(chunk)
162 162 if not self.sent_headers:
163 163 self.send_headers()
164 164 self._done()
165 165
166 166 def send_headers(self):
167 167 if not self.saved_status:
168 168 raise AssertionError("Sending headers before "
169 169 "start_response() called")
170 170 saved_status = self.saved_status.split(None, 1)
171 171 saved_status[0] = int(saved_status[0])
172 172 self.send_response(*saved_status)
173 173 self.length = None
174 174 self._chunked = False
175 175 for h in self.saved_headers:
176 176 self.send_header(*h)
177 177 if h[0].lower() == 'content-length':
178 178 self.length = int(h[1])
179 179 if (self.length is None and
180 180 saved_status[0] != common.HTTP_NOT_MODIFIED):
181 181 self._chunked = (not self.close_connection and
182 182 self.request_version == "HTTP/1.1")
183 183 if self._chunked:
184 184 self.send_header('Transfer-Encoding', 'chunked')
185 185 else:
186 186 self.send_header('Connection', 'close')
187 187 self.end_headers()
188 188 self.sent_headers = True
189 189
190 190 def _start_response(self, http_status, headers, exc_info=None):
191 191 code, msg = http_status.split(None, 1)
192 192 code = int(code)
193 193 self.saved_status = http_status
194 194 bad_headers = ('connection', 'transfer-encoding')
195 195 self.saved_headers = [h for h in headers
196 196 if h[0].lower() not in bad_headers]
197 197 return self._write
198 198
199 199 def _write(self, data):
200 200 if not self.saved_status:
201 201 raise AssertionError("data written before start_response() called")
202 202 elif not self.sent_headers:
203 203 self.send_headers()
204 204 if self.length is not None:
205 205 if len(data) > self.length:
206 206 raise AssertionError("Content-length header sent, but more "
207 207 "bytes than specified are being written.")
208 208 self.length = self.length - len(data)
209 209 elif self._chunked and data:
210 210 data = '%x\r\n%s\r\n' % (len(data), data)
211 211 self.wfile.write(data)
212 212 self.wfile.flush()
213 213
214 214 def _done(self):
215 215 if self._chunked:
216 216 self.wfile.write('0\r\n\r\n')
217 217 self.wfile.flush()
218 218
219 219 class _httprequesthandlerssl(_httprequesthandler):
220 220 """HTTPS handler based on Python's ssl module"""
221 221
222 222 url_scheme = 'https'
223 223
224 224 @staticmethod
225 225 def preparehttpserver(httpserver, ui):
226 226 try:
227 227 from .. import sslutil
228 228 sslutil.modernssl
229 229 except ImportError:
230 230 raise error.Abort(_("SSL support is unavailable"))
231 231
232 232 certfile = ui.config('web', 'certificate')
233 233
234 234 # These config options are currently only meant for testing. Use
235 235 # at your own risk.
236 236 cafile = ui.config('devel', 'servercafile')
237 237 reqcert = ui.configbool('devel', 'serverrequirecert')
238 238
239 239 httpserver.socket = sslutil.wrapserversocket(httpserver.socket,
240 240 ui,
241 241 certfile=certfile,
242 242 cafile=cafile,
243 243 requireclientcert=reqcert)
244 244
245 245 def setup(self):
246 246 self.connection = self.request
247 247 self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
248 248 self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
249 249
250 250 try:
251 251 import threading
252 252 threading.activeCount() # silence pyflakes and bypass demandimport
253 253 _mixin = socketserver.ThreadingMixIn
254 254 except ImportError:
255 255 if util.safehasattr(os, "fork"):
256 256 _mixin = socketserver.ForkingMixIn
257 257 else:
258 258 class _mixin(object):
259 259 pass
260 260
261 261 def openlog(opt, default):
262 262 if opt and opt != '-':
263 263 return open(opt, 'a')
264 264 return default
265 265
266 266 class MercurialHTTPServer(_mixin, httpservermod.httpserver, object):
267 267
268 268 # SO_REUSEADDR has broken semantics on windows
269 269 if pycompat.osname == 'nt':
270 270 allow_reuse_address = 0
271 271
272 272 def __init__(self, ui, app, addr, handler, **kwargs):
273 273 httpservermod.httpserver.__init__(self, addr, handler, **kwargs)
274 274 self.daemon_threads = True
275 275 self.application = app
276 276
277 277 handler.preparehttpserver(self, ui)
278 278
279 279 prefix = ui.config('web', 'prefix')
280 280 if prefix:
281 281 prefix = '/' + prefix.strip('/')
282 282 self.prefix = prefix
283 283
284 284 alog = openlog(ui.config('web', 'accesslog'), ui.fout)
285 285 elog = openlog(ui.config('web', 'errorlog'), ui.ferr)
286 286 self.accesslog = alog
287 287 self.errorlog = elog
288 288
289 289 self.addr, self.port = self.socket.getsockname()[0:2]
290 290 self.fqaddr = socket.getfqdn(addr[0])
291 291
292 292 class IPv6HTTPServer(MercurialHTTPServer):
293 293 address_family = getattr(socket, 'AF_INET6', None)
294 294 def __init__(self, *args, **kwargs):
295 295 if self.address_family is None:
296 296 raise error.RepoError(_('IPv6 is not available on this system'))
297 297 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
298 298
299 299 def create_server(ui, app):
300 300
301 301 if ui.config('web', 'certificate'):
302 302 handler = _httprequesthandlerssl
303 303 else:
304 304 handler = _httprequesthandler
305 305
306 306 if ui.configbool('web', 'ipv6'):
307 307 cls = IPv6HTTPServer
308 308 else:
309 309 cls = MercurialHTTPServer
310 310
311 311 # ugly hack due to python issue5853 (for threaded use)
312 312 try:
313 313 import mimetypes
314 314 mimetypes.init()
315 315 except UnicodeDecodeError:
316 316 # Python 2.x's mimetypes module attempts to decode strings
317 317 # from Windows' ANSI APIs as ascii (fail), then re-encode them
318 318 # as ascii (clown fail), because the default Python Unicode
319 319 # codec is hardcoded as ascii.
320 320
321 321 sys.argv # unwrap demand-loader so that reload() works
322 322 reload(sys) # resurrect sys.setdefaultencoding()
323 323 oldenc = sys.getdefaultencoding()
324 324 sys.setdefaultencoding("latin1") # or any full 8-bit encoding
325 325 mimetypes.init()
326 326 sys.setdefaultencoding(oldenc)
327 327
328 328 address = ui.config('web', 'address')
329 329 port = util.getport(ui.config('web', 'port'))
330 330 try:
331 331 return cls(ui, app, (address, port), handler)
332 332 except socket.error as inst:
333 333 raise error.Abort(_("cannot start server at '%s:%d': %s")
334 334 % (address, port, inst.args[1]))
@@ -1,90 +1,90 b''
1 1 # hgweb/wsgicgi.py - CGI->WSGI translator
2 2 #
3 3 # Copyright 2006 Eric Hopper <hopper@omnifarious.org>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 #
8 8 # This was originally copied from the public domain code at
9 9 # http://www.python.org/dev/peps/pep-0333/#the-server-gateway-side
10 10
11 11 from __future__ import absolute_import
12 12
13 13 from .. import (
14 14 encoding,
15 15 util,
16 16 )
17 17
18 18 from . import (
19 19 common,
20 20 )
21 21
22 22 def launch(application):
23 23 util.setbinary(util.stdin)
24 24 util.setbinary(util.stdout)
25 25
26 26 environ = dict(encoding.environ.iteritems())
27 environ.setdefault('PATH_INFO', '')
28 if environ.get('SERVER_SOFTWARE', '').startswith('Microsoft-IIS'):
27 environ.setdefault(r'PATH_INFO', '')
28 if environ.get(r'SERVER_SOFTWARE', r'').startswith(r'Microsoft-IIS'):
29 29 # IIS includes script_name in PATH_INFO
30 scriptname = environ['SCRIPT_NAME']
31 if environ['PATH_INFO'].startswith(scriptname):
32 environ['PATH_INFO'] = environ['PATH_INFO'][len(scriptname):]
30 scriptname = environ[r'SCRIPT_NAME']
31 if environ[r'PATH_INFO'].startswith(scriptname):
32 environ[r'PATH_INFO'] = environ[r'PATH_INFO'][len(scriptname):]
33 33
34 34 stdin = util.stdin
35 if environ.get('HTTP_EXPECT', '').lower() == '100-continue':
35 if environ.get(r'HTTP_EXPECT', r'').lower() == r'100-continue':
36 36 stdin = common.continuereader(stdin, util.stdout.write)
37 37
38 environ['wsgi.input'] = stdin
39 environ['wsgi.errors'] = util.stderr
40 environ['wsgi.version'] = (1, 0)
41 environ['wsgi.multithread'] = False
42 environ['wsgi.multiprocess'] = True
43 environ['wsgi.run_once'] = True
38 environ[r'wsgi.input'] = stdin
39 environ[r'wsgi.errors'] = util.stderr
40 environ[r'wsgi.version'] = (1, 0)
41 environ[r'wsgi.multithread'] = False
42 environ[r'wsgi.multiprocess'] = True
43 environ[r'wsgi.run_once'] = True
44 44
45 if environ.get('HTTPS', 'off').lower() in ('on', '1', 'yes'):
46 environ['wsgi.url_scheme'] = 'https'
45 if environ.get(r'HTTPS', r'off').lower() in (r'on', r'1', r'yes'):
46 environ[r'wsgi.url_scheme'] = r'https'
47 47 else:
48 environ['wsgi.url_scheme'] = 'http'
48 environ[r'wsgi.url_scheme'] = r'http'
49 49
50 50 headers_set = []
51 51 headers_sent = []
52 52 out = util.stdout
53 53
54 54 def write(data):
55 55 if not headers_set:
56 56 raise AssertionError("write() before start_response()")
57 57
58 58 elif not headers_sent:
59 59 # Before the first output, send the stored headers
60 60 status, response_headers = headers_sent[:] = headers_set
61 61 out.write('Status: %s\r\n' % status)
62 62 for header in response_headers:
63 63 out.write('%s: %s\r\n' % header)
64 64 out.write('\r\n')
65 65
66 66 out.write(data)
67 67 out.flush()
68 68
69 69 def start_response(status, response_headers, exc_info=None):
70 70 if exc_info:
71 71 try:
72 72 if headers_sent:
73 73 # Re-raise original exception if headers sent
74 74 raise exc_info[0](exc_info[1], exc_info[2])
75 75 finally:
76 76 exc_info = None # avoid dangling circular ref
77 77 elif headers_set:
78 78 raise AssertionError("Headers already set!")
79 79
80 80 headers_set[:] = [status, response_headers]
81 81 return write
82 82
83 83 content = application(environ, start_response)
84 84 try:
85 85 for chunk in content:
86 86 write(chunk)
87 87 if not headers_sent:
88 88 write('') # send headers now if body was empty
89 89 finally:
90 90 getattr(content, 'close', lambda: None)()
General Comments 0
You need to be logged in to leave comments. Login now