##// END OF EJS Templates
hgweb: remove Python 3 conditional...
Gregory Szorc -
r49760:7eebe563 default
parent child Browse files
Show More
@@ -1,634 +1,632
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 Olivia Mackall <olivia@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
10 10 # import wsgiref.validate
11 11
12 12 from ..thirdparty import attr
13 13 from .. import (
14 14 encoding,
15 15 error,
16 16 pycompat,
17 17 util,
18 18 )
19 19 from ..utils import (
20 20 urlutil,
21 21 )
22 22
23 23
24 24 class multidict(object):
25 25 """A dict like object that can store multiple values for a key.
26 26
27 27 Used to store parsed request parameters.
28 28
29 29 This is inspired by WebOb's class of the same name.
30 30 """
31 31
32 32 def __init__(self):
33 33 self._items = {}
34 34
35 35 def __getitem__(self, key):
36 36 """Returns the last set value for a key."""
37 37 return self._items[key][-1]
38 38
39 39 def __setitem__(self, key, value):
40 40 """Replace a values for a key with a new value."""
41 41 self._items[key] = [value]
42 42
43 43 def __delitem__(self, key):
44 44 """Delete all values for a key."""
45 45 del self._items[key]
46 46
47 47 def __contains__(self, key):
48 48 return key in self._items
49 49
50 50 def __len__(self):
51 51 return len(self._items)
52 52
53 53 def get(self, key, default=None):
54 54 try:
55 55 return self.__getitem__(key)
56 56 except KeyError:
57 57 return default
58 58
59 59 def add(self, key, value):
60 60 """Add a new value for a key. Does not replace existing values."""
61 61 self._items.setdefault(key, []).append(value)
62 62
63 63 def getall(self, key):
64 64 """Obtains all values for a key."""
65 65 return self._items.get(key, [])
66 66
67 67 def getone(self, key):
68 68 """Obtain a single value for a key.
69 69
70 70 Raises KeyError if key not defined or it has multiple values set.
71 71 """
72 72 vals = self._items[key]
73 73
74 74 if len(vals) > 1:
75 75 raise KeyError(b'multiple values for %r' % key)
76 76
77 77 return vals[0]
78 78
79 79 def asdictoflists(self):
80 80 return {k: list(v) for k, v in pycompat.iteritems(self._items)}
81 81
82 82
83 83 @attr.s(frozen=True)
84 84 class parsedrequest(object):
85 85 """Represents a parsed WSGI request.
86 86
87 87 Contains both parsed parameters as well as a handle on the input stream.
88 88 """
89 89
90 90 # Request method.
91 91 method = attr.ib()
92 92 # Full URL for this request.
93 93 url = attr.ib()
94 94 # URL without any path components. Just <proto>://<host><port>.
95 95 baseurl = attr.ib()
96 96 # Advertised URL. Like ``url`` and ``baseurl`` but uses SERVER_NAME instead
97 97 # of HTTP: Host header for hostname. This is likely what clients used.
98 98 advertisedurl = attr.ib()
99 99 advertisedbaseurl = attr.ib()
100 100 # URL scheme (part before ``://``). e.g. ``http`` or ``https``.
101 101 urlscheme = attr.ib()
102 102 # Value of REMOTE_USER, if set, or None.
103 103 remoteuser = attr.ib()
104 104 # Value of REMOTE_HOST, if set, or None.
105 105 remotehost = attr.ib()
106 106 # Relative WSGI application path. If defined, will begin with a
107 107 # ``/``.
108 108 apppath = attr.ib()
109 109 # List of path parts to be used for dispatch.
110 110 dispatchparts = attr.ib()
111 111 # URL path component (no query string) used for dispatch. Can be
112 112 # ``None`` to signal no path component given to the request, an
113 113 # empty string to signal a request to the application's root URL,
114 114 # or a string not beginning with ``/`` containing the requested
115 115 # path under the application.
116 116 dispatchpath = attr.ib()
117 117 # The name of the repository being accessed.
118 118 reponame = attr.ib()
119 119 # Raw query string (part after "?" in URL).
120 120 querystring = attr.ib()
121 121 # multidict of query string parameters.
122 122 qsparams = attr.ib()
123 123 # wsgiref.headers.Headers instance. Operates like a dict with case
124 124 # insensitive keys.
125 125 headers = attr.ib()
126 126 # Request body input stream.
127 127 bodyfh = attr.ib()
128 128 # WSGI environment dict, unmodified.
129 129 rawenv = attr.ib()
130 130
131 131
132 132 def parserequestfromenv(env, reponame=None, altbaseurl=None, bodyfh=None):
133 133 """Parse URL components from environment variables.
134 134
135 135 WSGI defines request attributes via environment variables. This function
136 136 parses the environment variables into a data structure.
137 137
138 138 If ``reponame`` is defined, the leading path components matching that
139 139 string are effectively shifted from ``PATH_INFO`` to ``SCRIPT_NAME``.
140 140 This simulates the world view of a WSGI application that processes
141 141 requests from the base URL of a repo.
142 142
143 143 If ``altbaseurl`` (typically comes from ``web.baseurl`` config option)
144 144 is defined, it is used - instead of the WSGI environment variables - for
145 145 constructing URL components up to and including the WSGI application path.
146 146 For example, if the current WSGI application is at ``/repo`` and a request
147 147 is made to ``/rev/@`` with this argument set to
148 148 ``http://myserver:9000/prefix``, the URL and path components will resolve as
149 149 if the request were to ``http://myserver:9000/prefix/rev/@``. In other
150 150 words, ``wsgi.url_scheme``, ``SERVER_NAME``, ``SERVER_PORT``, and
151 151 ``SCRIPT_NAME`` are all effectively replaced by components from this URL.
152 152
153 153 ``bodyfh`` can be used to specify a file object to read the request body
154 154 from. If not defined, ``wsgi.input`` from the environment dict is used.
155 155 """
156 156 # PEP 3333 defines the WSGI spec and is a useful reference for this code.
157 157
158 158 # We first validate that the incoming object conforms with the WSGI spec.
159 159 # We only want to be dealing with spec-conforming WSGI implementations.
160 160 # TODO enable this once we fix internal violations.
161 161 # wsgiref.validate.check_environ(env)
162 162
163 # PEP-0333 states that environment keys and values are native strings
164 # (bytes on Python 2 and str on Python 3). The code points for the Unicode
165 # strings on Python 3 must be between \00000-\000FF. We deal with bytes
166 # in Mercurial, so mass convert string keys and values to bytes.
167 if pycompat.ispy3:
163 # PEP-0333 states that environment keys and values are native strings.
164 # The code points for the Unicode strings on Python 3 must be between
165 # \00000-\000FF. We deal with bytes in Mercurial, so mass convert string
166 # keys and values to bytes.
167 def tobytes(s):
168 if not isinstance(s, str):
169 return s
170 if pycompat.iswindows:
171 # This is what mercurial.encoding does for os.environ on
172 # Windows.
173 return encoding.strtolocal(s)
174 else:
175 # This is what is documented to be used for os.environ on Unix.
176 return pycompat.fsencode(s)
168 177
169 def tobytes(s):
170 if not isinstance(s, str):
171 return s
172 if pycompat.iswindows:
173 # This is what mercurial.encoding does for os.environ on
174 # Windows.
175 return encoding.strtolocal(s)
176 else:
177 # This is what is documented to be used for os.environ on Unix.
178 return pycompat.fsencode(s)
179
180 env = {tobytes(k): tobytes(v) for k, v in pycompat.iteritems(env)}
178 env = {tobytes(k): tobytes(v) for k, v in pycompat.iteritems(env)}
181 179
182 180 # Some hosting solutions are emulating hgwebdir, and dispatching directly
183 181 # to an hgweb instance using this environment variable. This was always
184 182 # checked prior to d7fd203e36cc; keep doing so to avoid breaking them.
185 183 if not reponame:
186 184 reponame = env.get(b'REPO_NAME')
187 185
188 186 if altbaseurl:
189 187 altbaseurl = urlutil.url(altbaseurl)
190 188
191 189 # https://www.python.org/dev/peps/pep-0333/#environ-variables defines
192 190 # the environment variables.
193 191 # https://www.python.org/dev/peps/pep-0333/#url-reconstruction defines
194 192 # how URLs are reconstructed.
195 193 fullurl = env[b'wsgi.url_scheme'] + b'://'
196 194
197 195 if altbaseurl and altbaseurl.scheme:
198 196 advertisedfullurl = altbaseurl.scheme + b'://'
199 197 else:
200 198 advertisedfullurl = fullurl
201 199
202 200 def addport(s, port):
203 201 if s.startswith(b'https://'):
204 202 if port != b'443':
205 203 s += b':' + port
206 204 else:
207 205 if port != b'80':
208 206 s += b':' + port
209 207
210 208 return s
211 209
212 210 if env.get(b'HTTP_HOST'):
213 211 fullurl += env[b'HTTP_HOST']
214 212 else:
215 213 fullurl += env[b'SERVER_NAME']
216 214 fullurl = addport(fullurl, env[b'SERVER_PORT'])
217 215
218 216 if altbaseurl and altbaseurl.host:
219 217 advertisedfullurl += altbaseurl.host
220 218
221 219 if altbaseurl.port:
222 220 port = altbaseurl.port
223 221 elif altbaseurl.scheme == b'http' and not altbaseurl.port:
224 222 port = b'80'
225 223 elif altbaseurl.scheme == b'https' and not altbaseurl.port:
226 224 port = b'443'
227 225 else:
228 226 port = env[b'SERVER_PORT']
229 227
230 228 advertisedfullurl = addport(advertisedfullurl, port)
231 229 else:
232 230 advertisedfullurl += env[b'SERVER_NAME']
233 231 advertisedfullurl = addport(advertisedfullurl, env[b'SERVER_PORT'])
234 232
235 233 baseurl = fullurl
236 234 advertisedbaseurl = advertisedfullurl
237 235
238 236 fullurl += util.urlreq.quote(env.get(b'SCRIPT_NAME', b''))
239 237 fullurl += util.urlreq.quote(env.get(b'PATH_INFO', b''))
240 238
241 239 if altbaseurl:
242 240 path = altbaseurl.path or b''
243 241 if path and not path.startswith(b'/'):
244 242 path = b'/' + path
245 243 advertisedfullurl += util.urlreq.quote(path)
246 244 else:
247 245 advertisedfullurl += util.urlreq.quote(env.get(b'SCRIPT_NAME', b''))
248 246
249 247 advertisedfullurl += util.urlreq.quote(env.get(b'PATH_INFO', b''))
250 248
251 249 if env.get(b'QUERY_STRING'):
252 250 fullurl += b'?' + env[b'QUERY_STRING']
253 251 advertisedfullurl += b'?' + env[b'QUERY_STRING']
254 252
255 253 # If ``reponame`` is defined, that must be a prefix on PATH_INFO
256 254 # that represents the repository being dispatched to. When computing
257 255 # the dispatch info, we ignore these leading path components.
258 256
259 257 if altbaseurl:
260 258 apppath = altbaseurl.path or b''
261 259 if apppath and not apppath.startswith(b'/'):
262 260 apppath = b'/' + apppath
263 261 else:
264 262 apppath = env.get(b'SCRIPT_NAME', b'')
265 263
266 264 if reponame:
267 265 repoprefix = b'/' + reponame.strip(b'/')
268 266
269 267 if not env.get(b'PATH_INFO'):
270 268 raise error.ProgrammingError(b'reponame requires PATH_INFO')
271 269
272 270 if not env[b'PATH_INFO'].startswith(repoprefix):
273 271 raise error.ProgrammingError(
274 272 b'PATH_INFO does not begin with repo '
275 273 b'name: %s (%s)' % (env[b'PATH_INFO'], reponame)
276 274 )
277 275
278 276 dispatchpath = env[b'PATH_INFO'][len(repoprefix) :]
279 277
280 278 if dispatchpath and not dispatchpath.startswith(b'/'):
281 279 raise error.ProgrammingError(
282 280 b'reponame prefix of PATH_INFO does '
283 281 b'not end at path delimiter: %s (%s)'
284 282 % (env[b'PATH_INFO'], reponame)
285 283 )
286 284
287 285 apppath = apppath.rstrip(b'/') + repoprefix
288 286 dispatchparts = dispatchpath.strip(b'/').split(b'/')
289 287 dispatchpath = b'/'.join(dispatchparts)
290 288
291 289 elif b'PATH_INFO' in env:
292 290 if env[b'PATH_INFO'].strip(b'/'):
293 291 dispatchparts = env[b'PATH_INFO'].strip(b'/').split(b'/')
294 292 dispatchpath = b'/'.join(dispatchparts)
295 293 else:
296 294 dispatchparts = []
297 295 dispatchpath = b''
298 296 else:
299 297 dispatchparts = []
300 298 dispatchpath = None
301 299
302 300 querystring = env.get(b'QUERY_STRING', b'')
303 301
304 302 # We store as a list so we have ordering information. We also store as
305 303 # a dict to facilitate fast lookup.
306 304 qsparams = multidict()
307 305 for k, v in util.urlreq.parseqsl(querystring, keep_blank_values=True):
308 306 qsparams.add(k, v)
309 307
310 308 # HTTP_* keys contain HTTP request headers. The Headers structure should
311 309 # perform case normalization for us. We just rewrite underscore to dash
312 310 # so keys match what likely went over the wire.
313 311 headers = []
314 312 for k, v in pycompat.iteritems(env):
315 313 if k.startswith(b'HTTP_'):
316 314 headers.append((k[len(b'HTTP_') :].replace(b'_', b'-'), v))
317 315
318 316 from . import wsgiheaders # avoid cycle
319 317
320 318 headers = wsgiheaders.Headers(headers)
321 319
322 320 # This is kind of a lie because the HTTP header wasn't explicitly
323 321 # sent. But for all intents and purposes it should be OK to lie about
324 322 # this, since a consumer will either either value to determine how many
325 323 # bytes are available to read.
326 324 if b'CONTENT_LENGTH' in env and b'HTTP_CONTENT_LENGTH' not in env:
327 325 headers[b'Content-Length'] = env[b'CONTENT_LENGTH']
328 326
329 327 if b'CONTENT_TYPE' in env and b'HTTP_CONTENT_TYPE' not in env:
330 328 headers[b'Content-Type'] = env[b'CONTENT_TYPE']
331 329
332 330 if bodyfh is None:
333 331 bodyfh = env[b'wsgi.input']
334 332 if b'Content-Length' in headers:
335 333 bodyfh = util.cappedreader(
336 334 bodyfh, int(headers[b'Content-Length'] or b'0')
337 335 )
338 336
339 337 return parsedrequest(
340 338 method=env[b'REQUEST_METHOD'],
341 339 url=fullurl,
342 340 baseurl=baseurl,
343 341 advertisedurl=advertisedfullurl,
344 342 advertisedbaseurl=advertisedbaseurl,
345 343 urlscheme=env[b'wsgi.url_scheme'],
346 344 remoteuser=env.get(b'REMOTE_USER'),
347 345 remotehost=env.get(b'REMOTE_HOST'),
348 346 apppath=apppath,
349 347 dispatchparts=dispatchparts,
350 348 dispatchpath=dispatchpath,
351 349 reponame=reponame,
352 350 querystring=querystring,
353 351 qsparams=qsparams,
354 352 headers=headers,
355 353 bodyfh=bodyfh,
356 354 rawenv=env,
357 355 )
358 356
359 357
360 358 class offsettrackingwriter(object):
361 359 """A file object like object that is append only and tracks write count.
362 360
363 361 Instances are bound to a callable. This callable is called with data
364 362 whenever a ``write()`` is attempted.
365 363
366 364 Instances track the amount of written data so they can answer ``tell()``
367 365 requests.
368 366
369 367 The intent of this class is to wrap the ``write()`` function returned by
370 368 a WSGI ``start_response()`` function. Since ``write()`` is a callable and
371 369 not a file object, it doesn't implement other file object methods.
372 370 """
373 371
374 372 def __init__(self, writefn):
375 373 self._write = writefn
376 374 self._offset = 0
377 375
378 376 def write(self, s):
379 377 res = self._write(s)
380 378 # Some Python objects don't report the number of bytes written.
381 379 if res is None:
382 380 self._offset += len(s)
383 381 else:
384 382 self._offset += res
385 383
386 384 def flush(self):
387 385 pass
388 386
389 387 def tell(self):
390 388 return self._offset
391 389
392 390
393 391 class wsgiresponse(object):
394 392 """Represents a response to a WSGI request.
395 393
396 394 A response consists of a status line, headers, and a body.
397 395
398 396 Consumers must populate the ``status`` and ``headers`` fields and
399 397 make a call to a ``setbody*()`` method before the response can be
400 398 issued.
401 399
402 400 When it is time to start sending the response over the wire,
403 401 ``sendresponse()`` is called. It handles emitting the header portion
404 402 of the response message. It then yields chunks of body data to be
405 403 written to the peer. Typically, the WSGI application itself calls
406 404 and returns the value from ``sendresponse()``.
407 405 """
408 406
409 407 def __init__(self, req, startresponse):
410 408 """Create an empty response tied to a specific request.
411 409
412 410 ``req`` is a ``parsedrequest``. ``startresponse`` is the
413 411 ``start_response`` function passed to the WSGI application.
414 412 """
415 413 self._req = req
416 414 self._startresponse = startresponse
417 415
418 416 self.status = None
419 417 from . import wsgiheaders # avoid cycle
420 418
421 419 self.headers = wsgiheaders.Headers([])
422 420
423 421 self._bodybytes = None
424 422 self._bodygen = None
425 423 self._bodywillwrite = False
426 424 self._started = False
427 425 self._bodywritefn = None
428 426
429 427 def _verifybody(self):
430 428 if (
431 429 self._bodybytes is not None
432 430 or self._bodygen is not None
433 431 or self._bodywillwrite
434 432 ):
435 433 raise error.ProgrammingError(b'cannot define body multiple times')
436 434
437 435 def setbodybytes(self, b):
438 436 """Define the response body as static bytes.
439 437
440 438 The empty string signals that there is no response body.
441 439 """
442 440 self._verifybody()
443 441 self._bodybytes = b
444 442 self.headers[b'Content-Length'] = b'%d' % len(b)
445 443
446 444 def setbodygen(self, gen):
447 445 """Define the response body as a generator of bytes."""
448 446 self._verifybody()
449 447 self._bodygen = gen
450 448
451 449 def setbodywillwrite(self):
452 450 """Signal an intent to use write() to emit the response body.
453 451
454 452 **This is the least preferred way to send a body.**
455 453
456 454 It is preferred for WSGI applications to emit a generator of chunks
457 455 constituting the response body. However, some consumers can't emit
458 456 data this way. So, WSGI provides a way to obtain a ``write(data)``
459 457 function that can be used to synchronously perform an unbuffered
460 458 write.
461 459
462 460 Calling this function signals an intent to produce the body in this
463 461 manner.
464 462 """
465 463 self._verifybody()
466 464 self._bodywillwrite = True
467 465
468 466 def sendresponse(self):
469 467 """Send the generated response to the client.
470 468
471 469 Before this is called, ``status`` must be set and one of
472 470 ``setbodybytes()`` or ``setbodygen()`` must be called.
473 471
474 472 Calling this method multiple times is not allowed.
475 473 """
476 474 if self._started:
477 475 raise error.ProgrammingError(
478 476 b'sendresponse() called multiple times'
479 477 )
480 478
481 479 self._started = True
482 480
483 481 if not self.status:
484 482 raise error.ProgrammingError(b'status line not defined')
485 483
486 484 if (
487 485 self._bodybytes is None
488 486 and self._bodygen is None
489 487 and not self._bodywillwrite
490 488 ):
491 489 raise error.ProgrammingError(b'response body not defined')
492 490
493 491 # RFC 7232 Section 4.1 states that a 304 MUST generate one of
494 492 # {Cache-Control, Content-Location, Date, ETag, Expires, Vary}
495 493 # and SHOULD NOT generate other headers unless they could be used
496 494 # to guide cache updates. Furthermore, RFC 7230 Section 3.3.2
497 495 # states that no response body can be issued. Content-Length can
498 496 # be sent. But if it is present, it should be the size of the response
499 497 # that wasn't transferred.
500 498 if self.status.startswith(b'304 '):
501 499 # setbodybytes('') will set C-L to 0. This doesn't conform with the
502 500 # spec. So remove it.
503 501 if self.headers.get(b'Content-Length') == b'0':
504 502 del self.headers[b'Content-Length']
505 503
506 504 # Strictly speaking, this is too strict. But until it causes
507 505 # problems, let's be strict.
508 506 badheaders = {
509 507 k
510 508 for k in self.headers.keys()
511 509 if k.lower()
512 510 not in (
513 511 b'date',
514 512 b'etag',
515 513 b'expires',
516 514 b'cache-control',
517 515 b'content-location',
518 516 b'content-security-policy',
519 517 b'vary',
520 518 )
521 519 }
522 520 if badheaders:
523 521 raise error.ProgrammingError(
524 522 b'illegal header on 304 response: %s'
525 523 % b', '.join(sorted(badheaders))
526 524 )
527 525
528 526 if self._bodygen is not None or self._bodywillwrite:
529 527 raise error.ProgrammingError(
530 528 b"must use setbodybytes('') with 304 responses"
531 529 )
532 530
533 531 # Various HTTP clients (notably httplib) won't read the HTTP response
534 532 # until the HTTP request has been sent in full. If servers (us) send a
535 533 # response before the HTTP request has been fully sent, the connection
536 534 # may deadlock because neither end is reading.
537 535 #
538 536 # We work around this by "draining" the request data before
539 537 # sending any response in some conditions.
540 538 drain = False
541 539 close = False
542 540
543 541 # If the client sent Expect: 100-continue, we assume it is smart enough
544 542 # to deal with the server sending a response before reading the request.
545 543 # (httplib doesn't do this.)
546 544 if self._req.headers.get(b'Expect', b'').lower() == b'100-continue':
547 545 pass
548 546 # Only tend to request methods that have bodies. Strictly speaking,
549 547 # we should sniff for a body. But this is fine for our existing
550 548 # WSGI applications.
551 549 elif self._req.method not in (b'POST', b'PUT'):
552 550 pass
553 551 else:
554 552 # If we don't know how much data to read, there's no guarantee
555 553 # that we can drain the request responsibly. The WSGI
556 554 # specification only says that servers *should* ensure the
557 555 # input stream doesn't overrun the actual request. So there's
558 556 # no guarantee that reading until EOF won't corrupt the stream
559 557 # state.
560 558 if not isinstance(self._req.bodyfh, util.cappedreader):
561 559 close = True
562 560 else:
563 561 # We /could/ only drain certain HTTP response codes. But 200 and
564 562 # non-200 wire protocol responses both require draining. Since
565 563 # we have a capped reader in place for all situations where we
566 564 # drain, it is safe to read from that stream. We'll either do
567 565 # a drain or no-op if we're already at EOF.
568 566 drain = True
569 567
570 568 if close:
571 569 self.headers[b'Connection'] = b'Close'
572 570
573 571 if drain:
574 572 assert isinstance(self._req.bodyfh, util.cappedreader)
575 573 while True:
576 574 chunk = self._req.bodyfh.read(32768)
577 575 if not chunk:
578 576 break
579 577
580 578 strheaders = [
581 579 (pycompat.strurl(k), pycompat.strurl(v))
582 580 for k, v in self.headers.items()
583 581 ]
584 582 write = self._startresponse(pycompat.sysstr(self.status), strheaders)
585 583
586 584 if self._bodybytes:
587 585 yield self._bodybytes
588 586 elif self._bodygen:
589 587 for chunk in self._bodygen:
590 588 # PEP-3333 says that output must be bytes. And some WSGI
591 589 # implementations enforce this. We cast bytes-like types here
592 590 # for convenience.
593 591 if isinstance(chunk, bytearray):
594 592 chunk = bytes(chunk)
595 593
596 594 yield chunk
597 595 elif self._bodywillwrite:
598 596 self._bodywritefn = write
599 597 else:
600 598 error.ProgrammingError(b'do not know how to send body')
601 599
602 600 def getbodyfile(self):
603 601 """Obtain a file object like object representing the response body.
604 602
605 603 For this to work, you must call ``setbodywillwrite()`` and then
606 604 ``sendresponse()`` first. ``sendresponse()`` is a generator and the
607 605 function won't run to completion unless the generator is advanced. The
608 606 generator yields not items. The easiest way to consume it is with
609 607 ``list(res.sendresponse())``, which should resolve to an empty list -
610 608 ``[]``.
611 609 """
612 610 if not self._bodywillwrite:
613 611 raise error.ProgrammingError(b'must call setbodywillwrite() first')
614 612
615 613 if not self._started:
616 614 raise error.ProgrammingError(
617 615 b'must call sendresponse() first; did '
618 616 b'you remember to consume it since it '
619 617 b'is a generator?'
620 618 )
621 619
622 620 assert self._bodywritefn
623 621 return offsettrackingwriter(self._bodywritefn)
624 622
625 623
626 624 def wsgiapplication(app_maker):
627 625 """For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
628 626 can and should now be used as a WSGI application."""
629 627 application = app_maker()
630 628
631 629 def run_wsgi(env, respond):
632 630 return application(env, respond)
633 631
634 632 return run_wsgi
General Comments 0
You need to be logged in to leave comments. Login now