0%

PEP3333 (Part 3) -- WSGI 规范细节

这一部分主要是 WSGI 规范中的细节部分,涉及主要的方法(start_response 等)和对象(application_object 等),以及它们之间是如何交互,调用顺序如何,同时该遵守哪些约束,它们又是如何组合来实现 HTTP 协议的功能的。

规范细节

应用程序对象(application object)必须接受两个位置参数。为了更好的说明,我们将它们命名为 environ 以及 start_response,但是它们不是必须要这样子命名的。一个服务器或网关必须使用位置(而不是关键字)参数来调用应用程序对象。(例如上面例子的 result = application(environ, start_response))。

environ 变量是一个字典对象,包括 CGI 风格的环境变量。这个对象必须是 Python 的内建字典对象(不能是子类对象,例如 UserDict 或者其他类字典对象),而且应用程序要被允许可以任意地修改这个字典。这个字典必须包含某些 WSGI 所需要的变量(在下一节会描述),也要包括服务端特定地扩展变量,这些变量将根据下面描述的约定来命名。

start_response 参数是一个可以接收两个必需的位置参数以及一个可选参数的可调用对象。为了更好的说明,我们将这些参数命名为 statusresponse_headers 以及 exc_info。它们不是必须要这样子命名的。应用程序必须使用位置参数来调用这个 start_response 对象。(例如 start_response(status, response_headers))。

这个 start_response 必须返回一个带一个位置参数的可调用对象 write(body_data)。 这个位置参数是一个 bytestring ,用作 HTTP 响应体。(注意,可调用对象 write() 仅仅只能提供给包含强制输入 API 接口的某些现存的框架,新的应用或者框架都应该尽可能避免这样使用。有关更多细节,请参考《缓冲与流》部分)。

当服务器调用时,应用程序对象必须返回一个可以生成(yield)零个或更多 bytestring 的可迭代对象。这可以通过多种方式实现,比如返回一个 bytestring 列表,或者应用程序本身就是一个可以生成(yield)bytestring 字符串的生成器函数,又或者应用程序本身就是一个可以得到可迭代实例对象的类。不管是如何完成的,应用程序对象必须返回一个可以生成(yield)零个或更多 bytestring 字符串的可迭代对象

服务器或网关必须以无缓冲的形式向客户端发送所产生的 bytestring 字符串,在请求另一个 bytestring 字符串之前,必须完成前面所有 bytestring 字符串的传输。(换句话说,应用程序应该自己实现缓冲 buffer。请参阅下面的《缓冲与流》 部分,了解如何处理应用程序输出。)

如果调用 len(iterable) 成功,服务器应该可以确信这个结果是精确的。也就是受,如果这个应用程序返回的可迭代对象 iterable 提供了一个可工作的 __len__() 方法,那它必须是一个准确无误的结果(请参阅处理内容长度标题 Content-Length Header 部分,以了解这要如何正常使用。)

如果应用程序返回的可迭代对象包含 close() 方法,那么服务器或网关必须在当前请求结束的时候调用这个方法,不管这个请求时正常完成的,还是因为迭代请求过程中(iteration)由于应用程序错误提前终止的,抑或是浏览器失去连接导致的。(close() 方法要求支持应用程序释放资源的。这个协议旨在补充 PEP342 生成器支持的相关内容,因为其它常规的可迭代兑现给都会有 close() 方法)。

应用程序返回的生成器或者其他常规的可迭代对象都不应该假设整个可迭代对象会被消耗完,因为它可能会被服务器提前关闭 close。

注意,应用程序必须在可迭代对象生成它第一个响应体 bytestring 字符串之前调用 start_response 这个可调用对象,以便服务器可以在任何响应体产生之前发送响应头。但是,这个调用可以通过可迭代对象的首次迭代(生成)时执行,所以服务器不能假定这个 start_response() 在这个可迭代对象开始迭代之前就已经被调用。

最后,服务端和网关不能使用直接使用这个由应用程序返回的可迭代对象的任何其他属性,除非他是服务器或者网关指定的某个类型的实例,比如说是一个由 wsgi.file_wrapper 返回的 “类文件对象”(file wrapper),一般情况下,只有这里提到的属性,以及在 PEP 234 中定义的可访问的迭代 API 可以被访问。

environ 变量

environ 字典要求包含这些在 CGI(Common Gateway Interface)标准中定义的 CGI 环境变量。下面这些变量必须给出,除非它们的值可以为空字符串。在这种情况下,它们可以被忽略,除非另有说明:

变量
REQUEST_METHOD HTTP 请求方法,例如 GETPOST。这个值不能是空字符串,因此它是必填项
SCRIPT_NAME 请求 URL “路径”(path) 的初始部分,对应其应用程序对象,以便应用程序定位其虚拟 “地址”(location)。它可以是一个空字符串,如果应用程序对应的位置是服务器的 “root” 地址的话
PATH_INFO 请求 URL “路径”的剩余部分,用于指出这个请求目标的虚拟 “地址” 对应应用程序的那个部分。它可以是一个空字符串,如果请求 URL 的目标就是应用程序的根路径并且没有任何尾随斜杠指示的话
QUERY_STRING 请求 URL 中以 “?” 开头的部分(如果有的话)。它可以留空或者干脆不要
CONTENT_TYPE 在 HTTP 请求中 Content-Type 字段对应的内容。它可以留空或者干脆不要
CONTENT_LENGTH 在 HTTP 请求中 Content-Length 字段对应的内容。它可以留空或者干脆不要
SERVER_NAME/SERVER_PORT SCRIPT_NAMEPATH_INFO 一起提供的话,这两个字符串组合其实就完全可以代表这个 URL。但是要注意,如果提供了 HTTP_HOST ,那么就会优先使用 SERVER_NAME 去重新构建这个请求 URL。可以参考下面 URL 重构一节的内容。SERVER_NAMESERVER_PORT 都不能为空字符串,因此它们都是必填项。
SERVER_PROTOCOL 客户端用来发送请求的协议版本。这个值通常是 “HTTP/1.0” 或者 “HTTP/1.1” 之类的东西。他们可以被应用程序用来决定要如何处理任何 HTTP 请求头。(这个变量应该被叫做 REQUEST_PROTOCOL,因此它代表了实际上是请求所用到的协议,而不是服务器响应中必要使用的协议。但是,为了和 CGI 标准兼容,我们只能保留现有的名称
HTTP_ 开头的变量 表示那些由客户端提供的 HTTP 请求头对应起来的变量(这些变量会以 HTTP_ 开头)。这些变量的存在与否却决于是否在请求也有对应的 HTTP 头信息

服务器或网关应该尽可能多的提供其他 CGI 变量。此外,如果使用了 SSL,服务器或网关也应该尽可能多的提供 Apache SSL 环境变量,例如 HTTPS=on 或者 SSL_PROTOCOL。但是要注意,如果使用了不是上述所列举的必要的 CGI 变量的应用程序,在面对那些不支持相关扩展的 Web 服务器的时候,它就是不可移植的。(例如,如果一个 Web 服务器不支持发布文件的话,那么将不能提供如 DOCUMENT_ROOTPATH_TRANSLATED 之类的变量)。

一个符合 WSGI 规范的服务器或者网关都应该记录它能提供哪些环境变量,以及视情况而定给出这些变量的定义。应用程序应该检查它们所要的变量是否被提供了,以及当一个必要变量缺失时应该有一个回退计划(fallback plan)。

注意:丢失的变量(例如没有身份验证过程的话就不会有 REMOTE_USER 变量)应该从 environ 字典中去掉。同样要注意的是,所有提供出来的由 CGI 标准定义的变量的值都必须是 Native 字符串,如果 CGI 变量的值是其他 str 类型的话是违反标准的。

除了由 CGI 标准定义的变量之外,environ 字典还可以包含任意操作系统的 “环境变量”,并且必须包含以下 WSGI 标准定义的变量。

变量
wsgi.version tuple(1, 0) 代表 WSGI 1.0 标准
wsgi.url_scheme 代表应用程序要在哪里调用的 URL “Scheme” 部分的字符串。这个值通常是 “http” 或 “https”
wsgi.input 可以读取 HTTP 请求体字节的输入流(类文件对象)。服务器或网关可以根据应用程序的要求按需读取,或者它可以预先读取客户端的请求体并在内存或磁盘上缓冲他们,或者根据自己的偏好来使用任何技术来提供这样的输入流
wsgi.errors 为了标准化并尽可能集中地记录程序或其他地方所引起地错误,提供了这样一个可以写入错误输出地输出流(类文件对象)。这应该是一个“文本模式”(text mode)流,即应用程序应该在行末尾使用 \n,并假定它可以由服务器或网关转换为正确地行结尾。
(在其他以 str 类型为 Unicode 的平台上,这个错误流应该能接收并记录任何 unicode 字符并不引发错误。但是,它允许替换错误流其中的那些不能被流编码所解析呈现的字符)
对于许多服务器来说,wsgi.error 将是服务器的主要错误日志记录。它可能是 sys.stderr,也可能是排序好的日志文件。服务器的文档应该包含如何配置以及哪里可以找到记录输出的说明。如果需要的话,一个服务器或网关应该向不同的应用程序提供不同的错误流。
wsgi.multithread 如果应用程序可以由同一个进程的另一个线程同时调起,那么这个值应该为 true,否则应该考虑设为 false
wsgi.multiprocess 如果应用程序可以等效地(equivalent)被另外一个进程调起,那么这个值就为 true,否则应该考虑设为 false
wsgi.run_once 如果服务器或网关希望(但不能保证实现)应用程序在其包含的进程的生命周期内仅能被调用一次,那么这个值就应该为 true。一般情况下,如果网关是基于 CGI 或类似技术实现的话,这个值只能为 true

最后,这个 environ 字典也可以包含服务器自己定义的变量。这些变量只能使用“小写字母、数字、点以及下划线”来命名,并且应该使用服务器或网关定义的统一的前缀。例如,mod_python 可能定义类似于 mod_python.some_variable 命名的变量。

输入流和错误流

服务器提供的输入流和错误流必须包含下列方法:

方法 对应备注
read(size) input 流 1
readline() input 流 1,2
readlines(hint) input 流 1,3
__iter__() input 流
flush() errors 流 4
write(str) errors 流
writelines(seq) errors 流

每种方法的语义都在 Python 库定义(Python Library Reference)中被记录,除了上面表格中列出的备注之外:

  1. 服务器端不需要读取客户端指定的内容长度(Content-Length),而且如果应用程序试图读取这个数据,(服务器)就应该模拟出一个 EOF 条件给到应用程序。应用程序不应该读取长度超过内容长度(Content-Length)指定的数据。
    服务器应该允许 read() 方法的无参数调用,然后直接返回客户端输入流的剩余部分。
    当有应用程序试图从一个空的或者已经消费完毕的输入流中读取数据时,服务器端应该返回一个空的 bytestring 字符串。
  2. 服务器端应该支持 readline() 方法接收可选参数 size,但是在 WSGI 1.0 中,它们被允许不用支持这个实现。
    (在 WSGI 1.0 中,size 参数是不提供的,理由是这可能导致实现的复杂性,而且在实践中并不经常用到。但当 cgi 模块开始使用它(size 参数),所以实际的服务器不得不开始提供对它的支持。)
  3. 注意,readlines() 方法的参数 hint 对于调用方和实现方来说都是可有可无的。应用程序有权决定要不要提供它,而服务器端(网关侧)也有忽略这个参数的自由。
  4. 由于错误流不能被回退(rewound),所以服务器端(网关侧)可以接连的进行正向写操作(追加操作)而不用使用缓冲(buffering)。在这种情况下,flush() 方法实际上是一个空操作。但是,对于可移植应用程序来说,不能因此而假设这个输出流是无缓冲的或者 flush() 是一个空操作。为了确保输出已经被写入(到错误流),它们一定要调用 flush() 方法。(例如,最小化从多个进程写入到同一个错误日志的混合数据的时候)

所有遵守 WSGI 规范的服务器都应该要提供上表列出的方法。而遵守 WSGI 规范的应用程序则禁止使用其他输入流/错误流的其他方法和属性。特别是,应用程序不能试图去关闭流,即便它们提供了 close() 方法。

start_response() 可调用对象

第二个传递给应用程序对象的参数是形如 start_response(status, response_headers, exc_info=None) 的可调用对象。(根据 WSGI 的调用规则,参数必须以位置参数的形式提供而不能以关键字的形式)。这个 start_response 可调用对象被用来发起一个 HTTP 响应,它必须返回一个 write(body_data) 的可调用对象。

status 参数是一个 HTTP 状态字符串类似于 “200 OK” 或者 “404 Not Found.”。也就是说,这个字符串必须包含状态码(Status-Code)以及原因短语(Reason-Phrase),按顺序排列并且有空格隔开,周围不能有空格或其他字符(详见 RFC2616 第 6.1.1 节)。这个字符串不能包含控制功能字符,而且不能被回车换行或它们的组合结尾。

response_headers 参数是一个形如 (header_name, header_value) 元组的列表。它必须是一个 Python 列表(即 type(response_headers) is ListType),而且服务器端可以根据自己需要任意修改其中的内容。每一个 header_name 必须是有效的 HTTP 头字段名(在 RFC2616 中定义),而且不能有尾随的冒号或其他标点符号。

每一个 header_value 都不能包含控制功能字符,包括回车符或换行符(无论是其中嵌入的或者出现在结尾的)。这个要求使得服务器端(网关侧)/中间件响应处理程序在检查和修改响应头时,能尽量降低进行解析工作的复杂程度。

通常来说,服务器端(网关侧)负责确保发送给客户端的响应报头时正确的。如果应用程序省略了一个 HTTP 必需报头字段(或者违反了其他在规范中已经生效的约定),那么服务器或网关就必须要补充上去。比如说 HTTP Date: 字段和 Server: 字段通常是由服务器或网关侧提供的。

要提醒一下各位服务器端(网关侧)的开发者:HTTP 头字段名是不区分大小写的,所以在检查应用程序提供的头字段时一定要考虑这一点。

应用程序和中间件都被禁止使用 HTTP/1.1 中的 “逐跳首部”(hop-by-hop)功能或者头字段,同样的还有 HTTP/1.0 中等价的功能,或者其他可能影响到客户端到服务端连接持久性的任何报头。这些实际上是正式服务器应该处理的部分,对于任何试图发送这类型报头的应用程序,服务器端(网关侧)都应该认为这是一个致命的错误,同时如果它们提供了 start_response 对象,那么就应该抛出一个错误。(关于 “逐跳首部”(hop-by-hop)功能以及报头的规范可以参阅《HTTP 功能》一节)

在调用 start_response 时候,服务器应该要检查报头是否有错,以使得维持应用程序运作的时候能把错误抛出来。

实际上,start_response 可调用对象实际上是不能发送响应报头的。相反,它们必须为服务器端(网关侧)存储这些报头信息,直到当应用程序第一次返回一个空的 bytestring 字符串或应用程序第一次调用 write() 这个可调用对象的时候,才把这些值一并传输出去。也就是说,直到①产生了真实的响应体数据或者②应用程序返回的可迭代对象 iterable 已经迭代完成的时候,响应报头才会发送出去。(唯一的特例是响应头显式地指明了 Content-Length 内容长度为零地情况)。

“延迟响应报头传输”是为了确保直到最后一刻,缓冲池或者异步地应用程序依然可以用错误输出来替代原来预期地输出。例如,例如,如果错误发生在由应用程序缓冲生成响应体的时候,它就需要把原先的响应状态 “200 OK” 转换为 “500 Internal Error”。

如果提供了 exc_info 参数,它必须是 Python 的 sys.exc_info() 方法返回元组。这个参数当且仅当 start_response 由一个错误处理程序调用的时候,由应用程序本身提供。如果提供了 exc_info,那么不会有新的 HTTP 报头输出,start_response 应该使用 exc_info 提供的报头来替换原先存储的报头信息,这样就能让应用程序可以在错误发生时对于它要输出的内容 change its mind(“改变注意”)。

但是,如果提供了 exc_info,而且 HTTP 报头已经发送了,start_response 应该抛出一个错误,而且是应该使用 exc_info 元组重新抛出:

1
raise exc_info[1].with_traceback(exc_info[2])

这样就可以重新抛出应用程序已经捕获的错误,而且原则上这个时候应该终止应用程序。(一旦 HTTP 报头已经被发出去了,应用程序尝试把错误输出发给浏览器是不安全的)。如果 start_response 是带着 exc_info 参数调用的,那么应用程序就不能捕获任何由 start_response 抛出的错误。相反,它应该允许这些异常继续传递到服务器端(网关侧)层面。详见《错误处理》一节。

当且仅当提供了 exc_info 参数,应用程序就可能会多次调用 start_response() 方法。更确切地说,在 start_response 已经被【当前请求调用的应用程序】(the current invocation of the application) 调用过的情况下,如果不带 exc_info 再次调用 start_response 将是一个致命的错误。这也包括第一次调用 start_response 就报错的情况。

注意,服务器、网关或中间件在实现 start_response 的时候必须确保在函数执行体以外不要有对这个 exc_info 的引用,这是为了避免由于回溯(traceback)所导致的循环引用。最简单的做法是:

1
2
3
4
5
6
def start_response(status, response_headers, exc_info=None):
if exc_info:
try:
# do stuff w/exc_info here
finally:
exc_info = None # Avoid circular ref.

而本文示例中的 CGI 网关提供了关于这种技术的另一个例子。

处理 Content-Length 内容长度头字段

如果应用程序提供了 Content-Length 头,那么服务器不应该发送给客户端超过这个长度的字节,而且在发送足够的数据之后应该停止重复响应(response);如果应用程序这时还继续使用 write() 写数据,就应到抛出一个错误。(当然,如果应用程序没有提供足够的数据来满足这个头字段,服务器应当关闭连接并记录日志或者干脆抛出一个错误)

如果应用程序不提供 Content-Type 头字段,服务器端(网关侧)可以有几种方法可以处理它,最简单的做法是当响应已经完成就结束客户端的连接。

然而在某些情况下,服务器端(网关侧)可能能够得到一个 Content-Length 头,或者至少(服务器端)不想直接关闭客户端连接。如果应用程序不再调用 write(),而且返回一个 len() 值为 1 的可迭代对象(iterable),那么服务器端就可以自行通过可迭代对象 iterable 生成的第一个 bytestring 字符串来得到这个 Content-Length 的值。

而且,如果服务器和客户端都支持 HTTP/1.1 的 “分块编码”(chunked encoding),那么服务器可以使用 “分块编码” 来为每次 write() 调用或者可迭代对象每次生成的 bytestring 发送分块,那么就可以生成每个块对应的 Content-Length。如果希望这样做的话,可以允许服务器保持客户端的长连接。注意,服务器实现 “分块编码” 时必须完全遵守 RFC2616 规范,否则必须有其他策略来应对出现 Content-Length 确实的情况。

注意,应用程序和中间件不能对它们的输出应用任何类型的传输编码(Transfer-Encoding),例如分块(chunking)或压缩(gzipping)。像 “逐跳首部” (hop-by-hop)操作,这些编码应该是由实际的服务器端(网关侧)实现的。详见《HTTP 功能》一节。

缓冲与流

一般来说,应用程序会通过缓冲适度大小的输入内容然后一并输出来实现最佳吞吐量。在 Zope 这类现有的框架中,这是一种非常常见的做法:将输出内容缓冲在 StringIO 或类似的对象中,然后连同响应头一起传输。

对于应用程序来说,符合 WSGI 标准的对应做法是:简单地以 bytestring 字符串的形式返回一个包含响应体的、仅包含单一元素的可迭代对象(如列表)。这是绝大多数应用程序都推荐的做法,它可以使得 HTML 页面文本易于存储在内存中。

然而,对于大型文件来说,或者面对着的有特殊用途的 HTTP 流(例如 multipart 形式的 “服务器推送”(server push)),应用程序可能需要在更小的块(block)中提供它们的输出(例如避免将大文件一次过加载到内存中)。有时这会消耗一点时间去产生这种类型的响应(produce respone),但在此之前先发送部分响应还是很有用的。

在这种情况下,应用程序通常会返回一个迭代器(通常是一个生成器形式的迭代器),它负责以逐块逐块的形式提供输出内容。这些块可能会根据 multipart boundary (multipart 边界)字段的一致性被继续拆分(be broken)(对于“服务器推送”来说),或者仅仅只是为了抢在耗时任务之前发送而被拆分(比如要从磁盘文件中读取另一个块)。

WSGI 服务器、网关以及中间件都不能延迟任何块的传输。它们必须将块完全传输到客户端,同时要保证即便应用程序正在生成下一个块,它们也要继续传输。服务器端(网关侧)或中间件可以以以下三种方式之一提供这种保证:

  1. 在将控制权交还给应用程序之前,将整个块发送到操作系统(并请求刷新任何 操作系统 缓冲区),又或者,
  2. 使用不同的线程来确保应用程序在生成下一个块的时候继续传输块数据
  3. (仅针对中间件)发送整个块到它的父服务器端(网关侧)

通过提供这层保障,WSGI 规范就能做到让应用程序保证在输出数据的过程中,传输过程也不会在任意时间节点停顿。这对于基于 multipart 数据流的 “服务器推送” 功能的正确运行至关重要,其中被 multipart boundary (multipart 边界)字段所包裹的数据应该要完整的传输到客户端。

译者: 关于提到的 multipart boundary 相关知识点,可以查看 RFC 2388 中关于 multipart/form-data 数据形式的描述的 4.1 节,其实 multipart boundary 就是一个表示边界的字符串,为了和正文内容区分,它同时是比较复杂而杂乱无章的一个字符串。

块边界的中间件处理

为了更好的支持异步的应用程序以及服务器,中间件组件不能中断对应用程序返回的可迭代对象的迭代(循环)过程以获取多个值。如果中间件需要在应用程序的可迭代对象产生任何输出之前,从其中积累(accumulate)更多的数据,那么这个可迭代对象必须生成(yield)空的 bytestring 字符串。

为了满足这一个要求,另一种做法是,中间件组件必须在它底层应用程序的可迭代对象生成(yield)一个值的同时也至少生成(yield)一个值。如果中间件不能生成(yield)任何有意义的值,那么它就必须生成(yield)一个空的 bytestring 字符串。

这个要求确保了异步应用程序和服务器可以同步地做规划,以减少那些被要求用来运行给定数目的应用程序实例的线程的数目。

注意,这一要求意味着,只要中间件的底层应用程序返回是一个可迭代对象,那么它也必须返回一个可迭代对象。同时这个要求还禁止了中间件使用 write() 这个可调用方法去直接传输底层应用程序生成的数据。中间件只能用它父级服务器端的 write() 可调用对象去传输那些由底层应用程序用中间件自己的 write() 可调用对象发送过来的那些数据,中间件就跟管道一样。

write() 的可调用对象

一些现存的应用程序框架的 API 已不同于 WSGI 的形式提供了无缓冲的输出支持。具体来说,它们可能提供了 “write” 函数或者某种方法来往无缓冲的数据块中写入数据;或者它们提供的其实是一个有缓冲的 “write” 函数,同时有一个 “flush” 的机制来刷新缓冲区。

不幸的是,像这种 API 不能根据 WSGI 所使用的 “可迭代” 的应用程序对象返回的值来实现,除非用到了多线程或者其他特殊的机制。

因此,为了让这些框架能够继续使用这样一个无法避免的 API,WSGI 包含了一个特殊的 write() 可调用对象,由 start_response 可调用对象返回。

新的符合 WSGI 标准的应用程序和框架都应该尽可能避免使用 write() 这个可调用对象,因为 write() 可调用对象实际上是纯粹(strictly)为了支持这样一个无可避免的 API 的兼容方案(hack)。一般来说,应用程序应该通过它们返回的可迭代对象来提供输出,一位这使得 Web 服务器能够在同一个 Python 线程中交错其他任务的执行,从而为整个服务器提供更好的吞吐能力。

write() 可调用对象由 start_response() 可调用对象返回,它接受单一的参数:用来表示 HTTP 响应体的 bytestring 字符串。这个字符串虽然是入参,但是可以被产生输出的可迭代对象当作是由由它自己生成(yield)出来的一样。换句话说,在 write() 调用返回之前,write() 自己必须保证这个传入进来的入参,要么作为整体发送到客户端;或者被缓冲起来,直到应用程序继续执行到需要传输的时候一并发送出去(和其他 output data 表现一致)。

应用程序必须返回一个可迭代对象,尽管它可以直接使用 write() 来来生成部分或干脆整个响应体。这个返回的可迭代对象可能是“空的”(例如不产生非空的 bytestring 字符串),但如果它提供了不为空的 bytestring,这个输出必须交给服务器端(网关侧)正常处理(即马上发出去,或者排队)。应用程序不能在它们返回的可迭代对象中调用 write() ,因此,任何由可迭代对象 iterable 所产生的 bytestring 字符串都会在那些经过 write() 发送的 bytestring 发送完毕之后传输。

Unicode 编码问题

HTTP 协议并不直接支持 Unicode 编码,因此也没有相关的接口科研。所有的编码/解码操作必须由应用程序自己解决。所有发送到服务器或者从服务器接收的字符串,它们的类型必须是 str(Python2.x) 或者 bytes(Python3.x),而不能是 Unicode 类型。在返回的字符串对象中使用 Unicode 对象,这个行为是未被定义的。

注意传入 start_response 的字符串例如 status 或者 response headers 必须是遵守 RFC 2616 编码的字符串。因此,他们要不是 ISO-8859-1 编码的字符,抑或是使用 RFC 2047 定义的 MIME 编码。

在 Python 平台上,str 或者 String 类型实际上是基于 Unicode 编码的(如 Jython,IronPython 以及 Python3),本规范中所指的 “字符串” string 都只包含那些能够被 ISO-8859-1 标准(从 \u0000\u00FF)编码的码点(code points)。对于一个应用程序来说,提供包含其他 Unicode 编码的而字符或码点(code points)是一个致命错误。类似的,服务器和网关也不能提供其他包含其他 Unicode 编码的字符给到应用程序。

同样,本规范中提到的 “字符串” string 必须是 strString 类型的,而且不能是 unicode 或者 Unicode 类型的。而且,即便给定的平台允许 str/String 对象的字符可以使用超过 8 个比特位,也只有低 8 位的会被使用,以符合本规范中定义的 “字符串” 的值。

对于在本规范使用的 bytestring 字符串的值(例如从 wsgi.input 中读到的值,或者传入 write() 的值,或者由应用程序产生的值),这些值必须是 Python 3 中的 bytes 类型的值,或者是早期 Python 版本中 str 类型的值。

错误处理

一般情况下,应用程序应该尝试自己捕获自己的内部错误,并在浏览器中显示有用的信息(应用程序需要自行判断在对应的场景上下文中什么才是“有用”的信息)。

但是,为了显示这样的一个信息,应用程序就不能向浏览器发送任何实际的数据(也就是不能往输出写入东西,这些东西本身就是错误发生前写的),否则就会有破坏响应的危险。因此 WSGI 提供了一种机制,它允许应用程序发送它的错误信息,或者让它自动中止这次的响应:使用 start_response 中的 exc_info 参数。以下是一个使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
try:
# 这里应该补充应用程序的逻辑代码
status = "200 Froody"
response_headers = [("content-type", "text/plain")]
start_response(status, response_headers)
return ["normal body goes here"]
except:
# 在这个空白的 except 块之前应该在单独的处理程序中捕获
# 像 MemoryError, KeyboardInterrupt 等运行时问题
status = "500 Oops"
response_headers = [("content-type", "text/plain")]
start_response(status, response_headers, sys.exc_info())
return ["error body goes here"]

如果发生异常时没有输出被写入到流中,对 start_response 的调用就会正常返回,然后应用程序的可迭代对象会返回一个错误主体以发送到浏览器。但是如果有任何输出已经发送到浏览器了,start_response 将重新抛出给过来的异常。这个异常不应该被应用程序捕获,这样应用程序就会被中止。然后服务器端(网关侧)就可以捕获这个致命的异常然后终止响应。

服务器端应该捕获并记录任何会中止应用程序或其返回值(可迭代对象)循环过程的那些异常。如果当应用程序发生错误时,已经有一部分响应写入浏览器了,那么服务器端(网关侧)应该尽可能地往输出中加入错误信息内容(如果已经发送的报头明确指出使用 text/* 这种服务器可以知道如何完全修改的内容类型 content-type)。

一些中间件可能希望提供额外的异常处理服务,或者截获并替换调应用程序的错误信息。在这种情况下,中间件可能会选择补充重新提供 exc_info 信息给到 start_response,却而代之的是抛出一个中间件指定的异常,或者在存储完提供的参数之后直接返回无异常。这会导致应用程序返回他的错误主体可迭代对象(或者调用了 write()),从而允许中间件捕获并修改错误输出。这种技术的实现,需要应用程序开发者:

  1. 当开始响应错误的时候,始终提供 exc_info
  2. 不要捕获任何由 start_response (带 exc_info 参数调用)抛出的错误

HTTP 1.1 的 Expect/Continue

实现 HTTP 1.1 协议的服务器端(网关侧)必须为 HTTP 1.1 的 “Except/Continue” 机制提供清晰的支持。这包括通过几个途径实现:

  1. 对于包含 Expect: 100-continue 的请求必须即时返回一个的 “100 Continue” 的响应,然后正常继续。
  2. 正常继续请求处理的过程,但是当应用程序首次尝试从输入流中读取数据时,必须为应用程序提供一个 wsgi.input 流以便发送 “100 Continue” 响应。在直到客户端响应返回之前,应用程序读取 request 请求数据操作必须要保持阻塞状态。
  3. 等到客户端自行决定(或自行判断)服务器不支持 “Except/Continue” 时,自行发送请求体。(这是不理想的行为,因此不推荐)。

注意,这些行为限制不适用于 HTTP 1.0 协议的请求,也不适用于不针对应用程序对象的请求。有关更多 HTTP 1.1 Except/Continue 的细节,可以参看 RFC2616,8.2.3 节 以及 10.1.1 节。

其他 HTTP 功能

一般而言,服务器端(网关侧)应该学会“装疯卖傻”,允许应用程序可以完全控制自己的输出。它们应该只改变那些不会影响到应用程序响应语义有效性的那部分内容。应用程序开发人员总是可以自己添加中间件组件去为额外的(HTTP)功能提供支持,因此服务器端(网关侧)应该在对于其实现时应该保持保守的原则。从某种意义上来说,服务器端应该将自己当成是一个 “HTTP 网关服务器(gateway server)”,而应用程序端则把自己当成是一个 “HTTP 原始服务器(origin server)”。(参考 RFC2616 1.3 节有关术语的定义)

但是,由于 WSGI 服务器和应用程序不是通过 HTTP 协议通信的,因此 RFC2616 中关于 “逐跳首部” hop-by-hop 的报头不能用于 WSGI 内部通信中。基于 WSGI 的 Web 应用程序禁止生成任何 “逐跳首部” 的报头,无论是尝试使用某些可能会要求应用程序们生成类似报头的 HTTP 功能可能会要求应用程序们生成类似的报头,或者是要依靠 environ 字典中任何传入的 hop-by-hop 报头的内容。WSGI 服务器必须自己处理任何支持入站 “逐跳首部” 报头,比如通过解码任何入站的 Transfer-Encoding(传输编码)实现,如果可能的话也包括分块编码。

以上的原则可以应用在各种 HTTP 功能上。不过应该很清楚一件事(关于缓存)是,服务器可以通过 If-None-MatchIf-Modified-Since 请求头以及 Last-ModifiedETag 响应头来做缓存的校验。但是,这不是必须的,而且应用程序需要支持这个功能的话,应该自己做好缓存校验,因为服务器端(网关侧)不是要求去做这个校验的。

类似的,服务器端可能会对应用程序的响应进行重编码(re-encoding)或者传输编码(transport-encoding)。但是应用程序端应该自己使用一个合适的内容编码,并且不能使用传输编码。如果客户端要求,服务器端会传输应用程序响应的字节范围(byte ranges),并且应用程序端不能自发的支持字节范围。但是相反地,应用程序如果需要的话,可以自己执行这样一个函数。

注意,这些对应用程序端的约束不是必要的,这意味着,每一个应用程序都必须重新实现每一个 HTTP 功能。许多 HTTP 功能可以部分或全部交给中间件组件来实现,因此使得服务器开发者和应用程序开发者都可以从一遍又一遍的实现相同功能的噩梦中解放出来。

多线程支持

多线程支持与否取决于服务器。服务器如果可以并行的处理多个请求,那么也应该提供以单线程的形式运行应用程序的选项。以便那些不是线程安全的应用程序或框架都可以搭配这样的服务器。