Fixed issue-number mistake in NEWS update.
[python.git] / Lib / wsgiref / handlers.py
blob0af1c8f0bd403ea5f5151e304624f3c3d5f19005
1 """Base classes for server/gateway implementations"""
3 from types import StringType
4 from util import FileWrapper, guess_scheme, is_hop_by_hop
5 from headers import Headers
7 import sys, os, time
9 __all__ = ['BaseHandler', 'SimpleHandler', 'BaseCGIHandler', 'CGIHandler']
11 try:
12 dict
13 except NameError:
14 def dict(items):
15 d = {}
16 for k,v in items:
17 d[k] = v
18 return d
20 # Uncomment for 2.2 compatibility.
21 #try:
22 # True
23 # False
24 #except NameError:
25 # True = not None
26 # False = not True
29 # Weekday and month names for HTTP date/time formatting; always English!
30 _weekdayname = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
31 _monthname = [None, # Dummy so we can use 1-based month numbers
32 "Jan", "Feb", "Mar", "Apr", "May", "Jun",
33 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
35 def format_date_time(timestamp):
36 year, month, day, hh, mm, ss, wd, y, z = time.gmtime(timestamp)
37 return "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (
38 _weekdayname[wd], day, _monthname[month], year, hh, mm, ss
43 class BaseHandler:
44 """Manage the invocation of a WSGI application"""
46 # Configuration parameters; can override per-subclass or per-instance
47 wsgi_version = (1,0)
48 wsgi_multithread = True
49 wsgi_multiprocess = True
50 wsgi_run_once = False
52 origin_server = True # We are transmitting direct to client
53 http_version = "1.0" # Version that should be used for response
54 server_software = None # String name of server software, if any
56 # os_environ is used to supply configuration from the OS environment:
57 # by default it's a copy of 'os.environ' as of import time, but you can
58 # override this in e.g. your __init__ method.
59 os_environ = dict(os.environ.items())
61 # Collaborator classes
62 wsgi_file_wrapper = FileWrapper # set to None to disable
63 headers_class = Headers # must be a Headers-like class
65 # Error handling (also per-subclass or per-instance)
66 traceback_limit = None # Print entire traceback to self.get_stderr()
67 error_status = "500 Dude, this is whack!"
68 error_headers = [('Content-Type','text/plain')]
69 error_body = "A server error occurred. Please contact the administrator."
71 # State variables (don't mess with these)
72 status = result = None
73 headers_sent = False
74 headers = None
75 bytes_sent = 0
84 def run(self, application):
85 """Invoke the application"""
86 # Note to self: don't move the close()! Asynchronous servers shouldn't
87 # call close() from finish_response(), so if you close() anywhere but
88 # the double-error branch here, you'll break asynchronous servers by
89 # prematurely closing. Async servers must return from 'run()' without
90 # closing if there might still be output to iterate over.
91 try:
92 self.setup_environ()
93 self.result = application(self.environ, self.start_response)
94 self.finish_response()
95 except:
96 try:
97 self.handle_error()
98 except:
99 # If we get an error handling an error, just give up already!
100 self.close()
101 raise # ...and let the actual server figure it out.
104 def setup_environ(self):
105 """Set up the environment for one request"""
107 env = self.environ = self.os_environ.copy()
108 self.add_cgi_vars()
110 env['wsgi.input'] = self.get_stdin()
111 env['wsgi.errors'] = self.get_stderr()
112 env['wsgi.version'] = self.wsgi_version
113 env['wsgi.run_once'] = self.wsgi_run_once
114 env['wsgi.url_scheme'] = self.get_scheme()
115 env['wsgi.multithread'] = self.wsgi_multithread
116 env['wsgi.multiprocess'] = self.wsgi_multiprocess
118 if self.wsgi_file_wrapper is not None:
119 env['wsgi.file_wrapper'] = self.wsgi_file_wrapper
121 if self.origin_server and self.server_software:
122 env.setdefault('SERVER_SOFTWARE',self.server_software)
125 def finish_response(self):
126 """Send any iterable data, then close self and the iterable
128 Subclasses intended for use in asynchronous servers will
129 want to redefine this method, such that it sets up callbacks
130 in the event loop to iterate over the data, and to call
131 'self.close()' once the response is finished.
133 if not self.result_is_file() or not self.sendfile():
134 for data in self.result:
135 self.write(data)
136 self.finish_content()
137 self.close()
140 def get_scheme(self):
141 """Return the URL scheme being used"""
142 return guess_scheme(self.environ)
145 def set_content_length(self):
146 """Compute Content-Length or switch to chunked encoding if possible"""
147 try:
148 blocks = len(self.result)
149 except (TypeError,AttributeError,NotImplementedError):
150 pass
151 else:
152 if blocks==1:
153 self.headers['Content-Length'] = str(self.bytes_sent)
154 return
155 # XXX Try for chunked encoding if origin server and client is 1.1
158 def cleanup_headers(self):
159 """Make any necessary header changes or defaults
161 Subclasses can extend this to add other defaults.
163 if 'Content-Length' not in self.headers:
164 self.set_content_length()
166 def start_response(self, status, headers,exc_info=None):
167 """'start_response()' callable as specified by PEP 333"""
169 if exc_info:
170 try:
171 if self.headers_sent:
172 # Re-raise original exception if headers sent
173 raise exc_info[0], exc_info[1], exc_info[2]
174 finally:
175 exc_info = None # avoid dangling circular ref
176 elif self.headers is not None:
177 raise AssertionError("Headers already set!")
179 assert type(status) is StringType,"Status must be a string"
180 assert len(status)>=4,"Status must be at least 4 characters"
181 assert int(status[:3]),"Status message must begin w/3-digit code"
182 assert status[3]==" ", "Status message must have a space after code"
183 if __debug__:
184 for name,val in headers:
185 assert type(name) is StringType,"Header names must be strings"
186 assert type(val) is StringType,"Header values must be strings"
187 assert not is_hop_by_hop(name),"Hop-by-hop headers not allowed"
188 self.status = status
189 self.headers = self.headers_class(headers)
190 return self.write
193 def send_preamble(self):
194 """Transmit version/status/date/server, via self._write()"""
195 if self.origin_server:
196 if self.client_is_modern():
197 self._write('HTTP/%s %s\r\n' % (self.http_version,self.status))
198 if 'Date' not in self.headers:
199 self._write(
200 'Date: %s\r\n' % format_date_time(time.time())
202 if self.server_software and 'Server' not in self.headers:
203 self._write('Server: %s\r\n' % self.server_software)
204 else:
205 self._write('Status: %s\r\n' % self.status)
207 def write(self, data):
208 """'write()' callable as specified by PEP 333"""
210 assert type(data) is StringType,"write() argument must be string"
212 if not self.status:
213 raise AssertionError("write() before start_response()")
215 elif not self.headers_sent:
216 # Before the first output, send the stored headers
217 self.bytes_sent = len(data) # make sure we know content-length
218 self.send_headers()
219 else:
220 self.bytes_sent += len(data)
222 # XXX check Content-Length and truncate if too many bytes written?
223 self._write(data)
224 self._flush()
227 def sendfile(self):
228 """Platform-specific file transmission
230 Override this method in subclasses to support platform-specific
231 file transmission. It is only called if the application's
232 return iterable ('self.result') is an instance of
233 'self.wsgi_file_wrapper'.
235 This method should return a true value if it was able to actually
236 transmit the wrapped file-like object using a platform-specific
237 approach. It should return a false value if normal iteration
238 should be used instead. An exception can be raised to indicate
239 that transmission was attempted, but failed.
241 NOTE: this method should call 'self.send_headers()' if
242 'self.headers_sent' is false and it is going to attempt direct
243 transmission of the file.
245 return False # No platform-specific transmission by default
248 def finish_content(self):
249 """Ensure headers and content have both been sent"""
250 if not self.headers_sent:
251 self.headers['Content-Length'] = "0"
252 self.send_headers()
253 else:
254 pass # XXX check if content-length was too short?
256 def close(self):
257 """Close the iterable (if needed) and reset all instance vars
259 Subclasses may want to also drop the client connection.
261 try:
262 if hasattr(self.result,'close'):
263 self.result.close()
264 finally:
265 self.result = self.headers = self.status = self.environ = None
266 self.bytes_sent = 0; self.headers_sent = False
269 def send_headers(self):
270 """Transmit headers to the client, via self._write()"""
271 self.cleanup_headers()
272 self.headers_sent = True
273 if not self.origin_server or self.client_is_modern():
274 self.send_preamble()
275 self._write(str(self.headers))
278 def result_is_file(self):
279 """True if 'self.result' is an instance of 'self.wsgi_file_wrapper'"""
280 wrapper = self.wsgi_file_wrapper
281 return wrapper is not None and isinstance(self.result,wrapper)
284 def client_is_modern(self):
285 """True if client can accept status and headers"""
286 return self.environ['SERVER_PROTOCOL'].upper() != 'HTTP/0.9'
289 def log_exception(self,exc_info):
290 """Log the 'exc_info' tuple in the server log
292 Subclasses may override to retarget the output or change its format.
294 try:
295 from traceback import print_exception
296 stderr = self.get_stderr()
297 print_exception(
298 exc_info[0], exc_info[1], exc_info[2],
299 self.traceback_limit, stderr
301 stderr.flush()
302 finally:
303 exc_info = None
305 def handle_error(self):
306 """Log current error, and send error output to client if possible"""
307 self.log_exception(sys.exc_info())
308 if not self.headers_sent:
309 self.result = self.error_output(self.environ, self.start_response)
310 self.finish_response()
311 # XXX else: attempt advanced recovery techniques for HTML or text?
313 def error_output(self, environ, start_response):
314 """WSGI mini-app to create error output
316 By default, this just uses the 'error_status', 'error_headers',
317 and 'error_body' attributes to generate an output page. It can
318 be overridden in a subclass to dynamically generate diagnostics,
319 choose an appropriate message for the user's preferred language, etc.
321 Note, however, that it's not recommended from a security perspective to
322 spit out diagnostics to any old user; ideally, you should have to do
323 something special to enable diagnostic output, which is why we don't
324 include any here!
326 start_response(self.error_status,self.error_headers[:],sys.exc_info())
327 return [self.error_body]
330 # Pure abstract methods; *must* be overridden in subclasses
332 def _write(self,data):
333 """Override in subclass to buffer data for send to client
335 It's okay if this method actually transmits the data; BaseHandler
336 just separates write and flush operations for greater efficiency
337 when the underlying system actually has such a distinction.
339 raise NotImplementedError
341 def _flush(self):
342 """Override in subclass to force sending of recent '_write()' calls
344 It's okay if this method is a no-op (i.e., if '_write()' actually
345 sends the data.
347 raise NotImplementedError
349 def get_stdin(self):
350 """Override in subclass to return suitable 'wsgi.input'"""
351 raise NotImplementedError
353 def get_stderr(self):
354 """Override in subclass to return suitable 'wsgi.errors'"""
355 raise NotImplementedError
357 def add_cgi_vars(self):
358 """Override in subclass to insert CGI variables in 'self.environ'"""
359 raise NotImplementedError
371 class SimpleHandler(BaseHandler):
372 """Handler that's just initialized with streams, environment, etc.
374 This handler subclass is intended for synchronous HTTP/1.0 origin servers,
375 and handles sending the entire response output, given the correct inputs.
377 Usage::
379 handler = SimpleHandler(
380 inp,out,err,env, multithread=False, multiprocess=True
382 handler.run(app)"""
384 def __init__(self,stdin,stdout,stderr,environ,
385 multithread=True, multiprocess=False
387 self.stdin = stdin
388 self.stdout = stdout
389 self.stderr = stderr
390 self.base_env = environ
391 self.wsgi_multithread = multithread
392 self.wsgi_multiprocess = multiprocess
394 def get_stdin(self):
395 return self.stdin
397 def get_stderr(self):
398 return self.stderr
400 def add_cgi_vars(self):
401 self.environ.update(self.base_env)
403 def _write(self,data):
404 self.stdout.write(data)
405 self._write = self.stdout.write
407 def _flush(self):
408 self.stdout.flush()
409 self._flush = self.stdout.flush
412 class BaseCGIHandler(SimpleHandler):
414 """CGI-like systems using input/output/error streams and environ mapping
416 Usage::
418 handler = BaseCGIHandler(inp,out,err,env)
419 handler.run(app)
421 This handler class is useful for gateway protocols like ReadyExec and
422 FastCGI, that have usable input/output/error streams and an environment
423 mapping. It's also the base class for CGIHandler, which just uses
424 sys.stdin, os.environ, and so on.
426 The constructor also takes keyword arguments 'multithread' and
427 'multiprocess' (defaulting to 'True' and 'False' respectively) to control
428 the configuration sent to the application. It sets 'origin_server' to
429 False (to enable CGI-like output), and assumes that 'wsgi.run_once' is
430 False.
433 origin_server = False
453 class CGIHandler(BaseCGIHandler):
455 """CGI-based invocation via sys.stdin/stdout/stderr and os.environ
457 Usage::
459 CGIHandler().run(app)
461 The difference between this class and BaseCGIHandler is that it always
462 uses 'wsgi.run_once' of 'True', 'wsgi.multithread' of 'False', and
463 'wsgi.multiprocess' of 'True'. It does not take any initialization
464 parameters, but always uses 'sys.stdin', 'os.environ', and friends.
466 If you need to override any of these parameters, use BaseCGIHandler
467 instead.
470 wsgi_run_once = True
472 def __init__(self):
473 BaseCGIHandler.__init__(
474 self, sys.stdin, sys.stdout, sys.stderr, dict(os.environ.items()),
475 multithread=False, multiprocess=True