1ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) 2ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php 3ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh# Also licenced under the Apache License, 2.0: http://opensource.org/licenses/apache2.0.php 4ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh# Licensed to PSF under a Contributor Agreement 5ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh""" 6ffab958fd8d42ed7227d83007350e61555a1fa36Andrew HsiehMiddleware to check for obedience to the WSGI specification. 7ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 8ffab958fd8d42ed7227d83007350e61555a1fa36Andrew HsiehSome of the things this checks: 9ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 10ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh* Signature of the application and start_response (including that 11ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh keyword arguments are not used). 12ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 13ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh* Environment checks: 14ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 15ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh - Environment is a dictionary (and not a subclass). 16ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 17ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh - That all the required keys are in the environment: REQUEST_METHOD, 18ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh SERVER_NAME, SERVER_PORT, wsgi.version, wsgi.input, wsgi.errors, 19ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh wsgi.multithread, wsgi.multiprocess, wsgi.run_once 20ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 21ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh - That HTTP_CONTENT_TYPE and HTTP_CONTENT_LENGTH are not in the 22ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh environment (these headers should appear as CONTENT_LENGTH and 23ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh CONTENT_TYPE). 24ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 25ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh - Warns if QUERY_STRING is missing, as the cgi module acts 26ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh unpredictably in that case. 27ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 28ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh - That CGI-style variables (that don't contain a .) have 29ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh (non-unicode) string values 30ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 31ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh - That wsgi.version is a tuple 32ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 33ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh - That wsgi.url_scheme is 'http' or 'https' (@@: is this too 34ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh restrictive?) 35ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 36ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh - Warns if the REQUEST_METHOD is not known (@@: probably too 37ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh restrictive). 38ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 39ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh - That SCRIPT_NAME and PATH_INFO are empty or start with / 40ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 41ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh - That at least one of SCRIPT_NAME or PATH_INFO are set. 42ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 43ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh - That CONTENT_LENGTH is a positive integer. 44ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 45ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh - That SCRIPT_NAME is not '/' (it should be '', and PATH_INFO should 46ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh be '/'). 47ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 48ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh - That wsgi.input has the methods read, readline, readlines, and 49ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh __iter__ 50ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 51ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh - That wsgi.errors has the methods flush, write, writelines 52ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 53ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh* The status is a string, contains a space, starts with an integer, 54ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh and that integer is in range (> 100). 55ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 56ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh* That the headers is a list (not a subclass, not another kind of 57ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh sequence). 58ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 59ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh* That the items of the headers are tuples of strings. 60ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 61ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh* That there is no 'status' header (that is used in CGI, but not in 62ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh WSGI). 63ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 64ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh* That the headers don't contain newlines or colons, end in _ or -, or 65ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh contain characters codes below 037. 66ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 67ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh* That Content-Type is given if there is content (CGI often has a 68ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh default content type, but WSGI does not). 69ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 70ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh* That no Content-Type is given when there is no content (@@: is this 71ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh too restrictive?) 72ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 73ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh* That the exc_info argument to start_response is a tuple or None. 74ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 75ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh* That all calls to the writer are with strings, and no other methods 76ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh on the writer are accessed. 77ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 78ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh* That wsgi.input is used properly: 79ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 80ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh - .read() is called with zero or one argument 81ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 82ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh - That it returns a string 83ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 84ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh - That readline, readlines, and __iter__ return strings 85ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 86ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh - That .close() is not called 87ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 88ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh - No other methods are provided 89ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 90ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh* That wsgi.errors is used properly: 91ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 92ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh - .write() and .writelines() is called with a string 93ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 94ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh - That .close() is not called, and no other methods are provided. 95ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 96ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh* The response iterator: 97ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 98ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh - That it is not a string (it should be a list of a single string; a 99ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh string will work, but perform horribly). 100ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 101ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh - That .next() returns a string 102ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 103ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh - That the iterator is not iterated over until start_response has 104ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh been called (that can signal either a server or application 105ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh error). 106ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 107ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh - That .close() is called (doesn't raise exception, only prints to 108ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh sys.stderr, because we only know it isn't called when the object 109ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh is garbage collected). 110ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh""" 111ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh__all__ = ['validator'] 112ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 113ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 114ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehimport re 115ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehimport sys 116ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehfrom types import DictType, StringType, TupleType, ListType 117ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehimport warnings 118ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 119ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehheader_re = re.compile(r'^[a-zA-Z][a-zA-Z0-9\-_]*$') 120ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehbad_header_value_re = re.compile(r'[\000-\037]') 121ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 122ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehclass WSGIWarning(Warning): 123ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh """ 124ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh Raised in response to WSGI-spec-related warnings 125ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh """ 126ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 127ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehdef assert_(cond, *args): 128ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh if not cond: 129ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh raise AssertionError(*args) 130ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 131ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehdef validator(application): 132ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 133ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh """ 134ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh When applied between a WSGI server and a WSGI application, this 135ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh middleware will check for WSGI compliancy on a number of levels. 136ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh This middleware does not modify the request or response in any 137ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh way, but will raise an AssertionError if anything seems off 138ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh (except for a failure to close the application iterator, which 139ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh will be printed to stderr -- there's no way to raise an exception 140ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh at that point). 141ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh """ 142ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 143ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh def lint_app(*args, **kw): 144ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(len(args) == 2, "Two arguments required") 145ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(not kw, "No keyword arguments allowed") 146ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh environ, start_response = args 147ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 148ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh check_environ(environ) 149ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 150ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh # We use this to check if the application returns without 151ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh # calling start_response: 152ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh start_response_started = [] 153ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 154ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh def start_response_wrapper(*args, **kw): 155ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(len(args) == 2 or len(args) == 3, ( 156ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "Invalid number of arguments: %s" % (args,))) 157ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(not kw, "No keyword arguments allowed") 158ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh status = args[0] 159ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh headers = args[1] 160ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh if len(args) == 3: 161ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh exc_info = args[2] 162ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh else: 163ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh exc_info = None 164ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 165ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh check_status(status) 166ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh check_headers(headers) 167ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh check_content_type(status, headers) 168ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh check_exc_info(exc_info) 169ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 170ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh start_response_started.append(None) 171ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh return WriteWrapper(start_response(*args)) 172ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 173ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh environ['wsgi.input'] = InputWrapper(environ['wsgi.input']) 174ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh environ['wsgi.errors'] = ErrorWrapper(environ['wsgi.errors']) 175ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 176ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh iterator = application(environ, start_response_wrapper) 177ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(iterator is not None and iterator != False, 178ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "The application must return an iterator, if only an empty list") 179ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 180ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh check_iterator(iterator) 181ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 182ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh return IteratorWrapper(iterator, start_response_started) 183ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 184ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh return lint_app 185ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 186ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehclass InputWrapper: 187ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 188ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh def __init__(self, wsgi_input): 189ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh self.input = wsgi_input 190ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 191ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh def read(self, *args): 192ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(len(args) <= 1) 193ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh v = self.input.read(*args) 194ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(type(v) is type("")) 195ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh return v 196ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 197ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh def readline(self): 198ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh v = self.input.readline() 199ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(type(v) is type("")) 200ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh return v 201ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 202ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh def readlines(self, *args): 203ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(len(args) <= 1) 204ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh lines = self.input.readlines(*args) 205ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(type(lines) is type([])) 206ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh for line in lines: 207ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(type(line) is type("")) 208ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh return lines 209ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 210ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh def __iter__(self): 211ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh while 1: 212ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh line = self.readline() 213ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh if not line: 214ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh return 215ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh yield line 216ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 217ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh def close(self): 218ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(0, "input.close() must not be called") 219ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 220ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehclass ErrorWrapper: 221ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 222ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh def __init__(self, wsgi_errors): 223ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh self.errors = wsgi_errors 224ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 225ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh def write(self, s): 226ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(type(s) is type("")) 227ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh self.errors.write(s) 228ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 229ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh def flush(self): 230ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh self.errors.flush() 231ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 232ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh def writelines(self, seq): 233ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh for line in seq: 234ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh self.write(line) 235ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 236ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh def close(self): 237ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(0, "errors.close() must not be called") 238ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 239ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehclass WriteWrapper: 240ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 241ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh def __init__(self, wsgi_writer): 242ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh self.writer = wsgi_writer 243ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 244ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh def __call__(self, s): 245ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(type(s) is type("")) 246ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh self.writer(s) 247ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 248ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehclass PartialIteratorWrapper: 249ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 250ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh def __init__(self, wsgi_iterator): 251ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh self.iterator = wsgi_iterator 252ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 253ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh def __iter__(self): 254ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh # We want to make sure __iter__ is called 255ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh return IteratorWrapper(self.iterator, None) 256ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 257ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehclass IteratorWrapper: 258ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 259ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh def __init__(self, wsgi_iterator, check_start_response): 260ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh self.original_iterator = wsgi_iterator 261ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh self.iterator = iter(wsgi_iterator) 262ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh self.closed = False 263ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh self.check_start_response = check_start_response 264ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 265ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh def __iter__(self): 266ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh return self 267ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 268ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh def next(self): 269ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(not self.closed, 270ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "Iterator read after closed") 271ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh v = self.iterator.next() 272ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh if self.check_start_response is not None: 273ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(self.check_start_response, 274ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "The application returns and we started iterating over its body, but start_response has not yet been called") 275ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh self.check_start_response = None 276ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh return v 277ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 278ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh def close(self): 279ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh self.closed = True 280ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh if hasattr(self.original_iterator, 'close'): 281ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh self.original_iterator.close() 282ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 283ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh def __del__(self): 284ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh if not self.closed: 285ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh sys.stderr.write( 286ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "Iterator garbage collected without being closed") 287ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(self.closed, 288ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "Iterator garbage collected without being closed") 289ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 290ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehdef check_environ(environ): 291ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(type(environ) is DictType, 292ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "Environment is not of the right type: %r (environment: %r)" 293ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh % (type(environ), environ)) 294ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 295ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh for key in ['REQUEST_METHOD', 'SERVER_NAME', 'SERVER_PORT', 296ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 'wsgi.version', 'wsgi.input', 'wsgi.errors', 297ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 'wsgi.multithread', 'wsgi.multiprocess', 298ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 'wsgi.run_once']: 299ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(key in environ, 300ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "Environment missing required key: %r" % (key,)) 301ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 302ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh for key in ['HTTP_CONTENT_TYPE', 'HTTP_CONTENT_LENGTH']: 303ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(key not in environ, 304ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "Environment should not have the key: %s " 305ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "(use %s instead)" % (key, key[5:])) 306ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 307ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh if 'QUERY_STRING' not in environ: 308ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh warnings.warn( 309ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 'QUERY_STRING is not in the WSGI environment; the cgi ' 310ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 'module will use sys.argv when this variable is missing, ' 311ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 'so application errors are more likely', 312ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh WSGIWarning) 313ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 314ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh for key in environ.keys(): 315ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh if '.' in key: 316ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh # Extension, we don't care about its type 317ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh continue 318ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(type(environ[key]) is StringType, 319ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "Environmental variable %s is not a string: %r (value: %r)" 320ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh % (key, type(environ[key]), environ[key])) 321ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 322ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(type(environ['wsgi.version']) is TupleType, 323ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "wsgi.version should be a tuple (%r)" % (environ['wsgi.version'],)) 324ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(environ['wsgi.url_scheme'] in ('http', 'https'), 325ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "wsgi.url_scheme unknown: %r" % environ['wsgi.url_scheme']) 326ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 327ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh check_input(environ['wsgi.input']) 328ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh check_errors(environ['wsgi.errors']) 329ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 330ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh # @@: these need filling out: 331ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh if environ['REQUEST_METHOD'] not in ( 332ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 'GET', 'HEAD', 'POST', 'OPTIONS','PUT','DELETE','TRACE'): 333ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh warnings.warn( 334ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "Unknown REQUEST_METHOD: %r" % environ['REQUEST_METHOD'], 335ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh WSGIWarning) 336ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 337ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(not environ.get('SCRIPT_NAME') 338ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh or environ['SCRIPT_NAME'].startswith('/'), 339ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "SCRIPT_NAME doesn't start with /: %r" % environ['SCRIPT_NAME']) 340ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(not environ.get('PATH_INFO') 341ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh or environ['PATH_INFO'].startswith('/'), 342ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "PATH_INFO doesn't start with /: %r" % environ['PATH_INFO']) 343ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh if environ.get('CONTENT_LENGTH'): 344ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(int(environ['CONTENT_LENGTH']) >= 0, 345ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "Invalid CONTENT_LENGTH: %r" % environ['CONTENT_LENGTH']) 346ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 347ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh if not environ.get('SCRIPT_NAME'): 348ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_('PATH_INFO' in environ, 349ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "One of SCRIPT_NAME or PATH_INFO are required (PATH_INFO " 350ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "should at least be '/' if SCRIPT_NAME is empty)") 351ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(environ.get('SCRIPT_NAME') != '/', 352ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "SCRIPT_NAME cannot be '/'; it should instead be '', and " 353ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "PATH_INFO should be '/'") 354ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 355ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehdef check_input(wsgi_input): 356ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh for attr in ['read', 'readline', 'readlines', '__iter__']: 357ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(hasattr(wsgi_input, attr), 358ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "wsgi.input (%r) doesn't have the attribute %s" 359ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh % (wsgi_input, attr)) 360ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 361ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehdef check_errors(wsgi_errors): 362ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh for attr in ['flush', 'write', 'writelines']: 363ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(hasattr(wsgi_errors, attr), 364ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "wsgi.errors (%r) doesn't have the attribute %s" 365ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh % (wsgi_errors, attr)) 366ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 367ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehdef check_status(status): 368ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(type(status) is StringType, 369ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "Status must be a string (not %r)" % status) 370ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh # Implicitly check that we can turn it into an integer: 371ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh status_code = status.split(None, 1)[0] 372ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(len(status_code) == 3, 373ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "Status codes must be three characters: %r" % status_code) 374ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh status_int = int(status_code) 375ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(status_int >= 100, "Status code is invalid: %r" % status_int) 376ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh if len(status) < 4 or status[3] != ' ': 377ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh warnings.warn( 378ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "The status string (%r) should be a three-digit integer " 379ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "followed by a single space and a status explanation" 380ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh % status, WSGIWarning) 381ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 382ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehdef check_headers(headers): 383ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(type(headers) is ListType, 384ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "Headers (%r) must be of type list: %r" 385ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh % (headers, type(headers))) 386ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh header_names = {} 387ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh for item in headers: 388ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(type(item) is TupleType, 389ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "Individual headers (%r) must be of type tuple: %r" 390ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh % (item, type(item))) 391ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(len(item) == 2) 392ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh name, value = item 393ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(name.lower() != 'status', 394ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "The Status header cannot be used; it conflicts with CGI " 395ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "script, and HTTP status is not given through headers " 396ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "(value: %r)." % value) 397ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh header_names[name.lower()] = None 398ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_('\n' not in name and ':' not in name, 399ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "Header names may not contain ':' or '\\n': %r" % name) 400ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(header_re.search(name), "Bad header name: %r" % name) 401ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(not name.endswith('-') and not name.endswith('_'), 402ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "Names may not end in '-' or '_': %r" % name) 403ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh if bad_header_value_re.search(value): 404ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(0, "Bad header value: %r (bad char: %r)" 405ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh % (value, bad_header_value_re.search(value).group(0))) 406ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 407ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehdef check_content_type(status, headers): 408ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh code = int(status.split(None, 1)[0]) 409ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh # @@: need one more person to verify this interpretation of RFC 2616 410ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html 411ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh NO_MESSAGE_BODY = (204, 304) 412ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh for name, value in headers: 413ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh if name.lower() == 'content-type': 414ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh if code not in NO_MESSAGE_BODY: 415ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh return 416ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(0, ("Content-Type header found in a %s response, " 417ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "which must not return content.") % code) 418ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh if code not in NO_MESSAGE_BODY: 419ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(0, "No Content-Type header found in headers (%s)" % headers) 420ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 421ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehdef check_exc_info(exc_info): 422ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(exc_info is None or type(exc_info) is type(()), 423ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "exc_info (%r) is not a tuple: %r" % (exc_info, type(exc_info))) 424ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh # More exc_info checks? 425ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh 426ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehdef check_iterator(iterator): 427ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh # Technically a string is legal, which is why it's a really bad 428ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh # idea, because it may cause the response to be returned 429ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh # character-by-character 430ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh assert_(not isinstance(iterator, str), 431ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "You should not return a string as your application iterator, " 432ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh "instead return a single-item list containing that string.") 433