comparison mercurial/hgweb/request.py @ 36867:a88d68dc3ee8

hgweb: create dedicated type for WSGI responses We have refactored the request side of WSGI processing into a dedicated type. Now let's do the same thing for the response side. We invent a ``wsgiresponse`` type. It takes an instance of a request (for consulation) and the WSGI application's "start_response" handler. The type basically allows setting the HTTP status line, response headers, and the response body. The WSGI application calls sendresponse() to start sending output. Output is emitted as a generator to be fed through the WSGI application. According to PEP 3333, this is the preferred way for output to be transmitted. (Our legacy ``wsgirequest`` exposed a write() to send data. We do not wish to support this API because it isn't recommended by PEP 3333.) The wire protocol code has been ported to use the new API. Differential Revision: https://phab.mercurial-scm.org/D2775
author Gregory Szorc <gregory.szorc@gmail.com>
date Sat, 10 Mar 2018 11:23:05 -0800
parents 422be99519e5
children ec0af9c59270
comparison
equal deleted inserted replaced
36866:2859c6fa4fc2 36867:a88d68dc3ee8
21 21
22 from ..thirdparty import ( 22 from ..thirdparty import (
23 attr, 23 attr,
24 ) 24 )
25 from .. import ( 25 from .. import (
26 error,
26 pycompat, 27 pycompat,
27 util, 28 util,
28 ) 29 )
29 30
30 @attr.s(frozen=True) 31 @attr.s(frozen=True)
199 querystringlist=querystringlist, 200 querystringlist=querystringlist,
200 querystringdict=querystringdict, 201 querystringdict=querystringdict,
201 headers=headers, 202 headers=headers,
202 bodyfh=bodyfh) 203 bodyfh=bodyfh)
203 204
205 class wsgiresponse(object):
206 """Represents a response to a WSGI request.
207
208 A response consists of a status line, headers, and a body.
209
210 Consumers must populate the ``status`` and ``headers`` fields and
211 make a call to a ``setbody*()`` method before the response can be
212 issued.
213
214 When it is time to start sending the response over the wire,
215 ``sendresponse()`` is called. It handles emitting the header portion
216 of the response message. It then yields chunks of body data to be
217 written to the peer. Typically, the WSGI application itself calls
218 and returns the value from ``sendresponse()``.
219 """
220
221 def __init__(self, req, startresponse):
222 """Create an empty response tied to a specific request.
223
224 ``req`` is a ``parsedrequest``. ``startresponse`` is the
225 ``start_response`` function passed to the WSGI application.
226 """
227 self._req = req
228 self._startresponse = startresponse
229
230 self.status = None
231 self.headers = wsgiheaders.Headers([])
232
233 self._bodybytes = None
234 self._bodygen = None
235 self._started = False
236
237 def setbodybytes(self, b):
238 """Define the response body as static bytes."""
239 if self._bodybytes is not None or self._bodygen is not None:
240 raise error.ProgrammingError('cannot define body multiple times')
241
242 self._bodybytes = b
243 self.headers['Content-Length'] = '%d' % len(b)
244
245 def setbodygen(self, gen):
246 """Define the response body as a generator of bytes."""
247 if self._bodybytes is not None or self._bodygen is not None:
248 raise error.ProgrammingError('cannot define body multiple times')
249
250 self._bodygen = gen
251
252 def sendresponse(self):
253 """Send the generated response to the client.
254
255 Before this is called, ``status`` must be set and one of
256 ``setbodybytes()`` or ``setbodygen()`` must be called.
257
258 Calling this method multiple times is not allowed.
259 """
260 if self._started:
261 raise error.ProgrammingError('sendresponse() called multiple times')
262
263 self._started = True
264
265 if not self.status:
266 raise error.ProgrammingError('status line not defined')
267
268 if self._bodybytes is None and self._bodygen is None:
269 raise error.ProgrammingError('response body not defined')
270
271 # Various HTTP clients (notably httplib) won't read the HTTP response
272 # until the HTTP request has been sent in full. If servers (us) send a
273 # response before the HTTP request has been fully sent, the connection
274 # may deadlock because neither end is reading.
275 #
276 # We work around this by "draining" the request data before
277 # sending any response in some conditions.
278 drain = False
279 close = False
280
281 # If the client sent Expect: 100-continue, we assume it is smart enough
282 # to deal with the server sending a response before reading the request.
283 # (httplib doesn't do this.)
284 if self._req.headers.get('Expect', '').lower() == '100-continue':
285 pass
286 # Only tend to request methods that have bodies. Strictly speaking,
287 # we should sniff for a body. But this is fine for our existing
288 # WSGI applications.
289 elif self._req.method not in ('POST', 'PUT'):
290 pass
291 else:
292 # If we don't know how much data to read, there's no guarantee
293 # that we can drain the request responsibly. The WSGI
294 # specification only says that servers *should* ensure the
295 # input stream doesn't overrun the actual request. So there's
296 # no guarantee that reading until EOF won't corrupt the stream
297 # state.
298 if not isinstance(self._req.bodyfh, util.cappedreader):
299 close = True
300 else:
301 # We /could/ only drain certain HTTP response codes. But 200 and
302 # non-200 wire protocol responses both require draining. Since
303 # we have a capped reader in place for all situations where we
304 # drain, it is safe to read from that stream. We'll either do
305 # a drain or no-op if we're already at EOF.
306 drain = True
307
308 if close:
309 self.headers['Connection'] = 'Close'
310
311 if drain:
312 assert isinstance(self._req.bodyfh, util.cappedreader)
313 while True:
314 chunk = self._req.bodyfh.read(32768)
315 if not chunk:
316 break
317
318 self._startresponse(pycompat.sysstr(self.status), self.headers.items())
319 if self._bodybytes:
320 yield self._bodybytes
321 elif self._bodygen:
322 for chunk in self._bodygen:
323 yield chunk
324 else:
325 error.ProgrammingError('do not know how to send body')
326
204 class wsgirequest(object): 327 class wsgirequest(object):
205 """Higher-level API for a WSGI request. 328 """Higher-level API for a WSGI request.
206 329
207 WSGI applications are invoked with 2 arguments. They are used to 330 WSGI applications are invoked with 2 arguments. They are used to
208 instantiate instances of this class, which provides higher-level APIs 331 instantiate instances of this class, which provides higher-level APIs
226 self.multiprocess = wsgienv[r'wsgi.multiprocess'] 349 self.multiprocess = wsgienv[r'wsgi.multiprocess']
227 self.run_once = wsgienv[r'wsgi.run_once'] 350 self.run_once = wsgienv[r'wsgi.run_once']
228 self.env = wsgienv 351 self.env = wsgienv
229 self.req = parserequestfromenv(wsgienv, inp) 352 self.req = parserequestfromenv(wsgienv, inp)
230 self.form = self.req.querystringdict 353 self.form = self.req.querystringdict
354 self.res = wsgiresponse(self.req, start_response)
231 self._start_response = start_response 355 self._start_response = start_response
232 self.server_write = None 356 self.server_write = None
233 self.headers = [] 357 self.headers = []
234 358
235 def respond(self, status, type, filename=None, body=None): 359 def respond(self, status, type, filename=None, body=None):