如何在 uwsgi 中间件中更改响应和 content-length?
How to change response and content-length in uwsgi middleware?
我正在尝试编写一个中间件来替换响应中的一些数据,从而改变内容长度。对于我们的开发环境,我们想要模拟 SSI 的行为,包括实际网络服务器(如 Nginx 或 Apache)的一些静态文件,这些文件不是通过应用程序提供的。我们正在使用 werkzeug 包含的开发服务器。
这是我目前的情况:
class ModifyBodyMiddleware(object):
def __init__(self, app):
self.app = app
def __call__(self, environment, start_response):
def my_start_response(status, headers, exc_info=None):
# change content-length somehow
start_response(status, headers, exc_info)
body = self.app(environment, my_start_response)
body = do_modifications(body)
return body
为简化起见,假设 do_modifications
确实用 foobar
替换了整个内容。我需要实际的 body 来修改它,但我还需要以某种方式设置新的 content-length header。
谢谢
戈尔
你想修改内容的什么地方?是否应该只对某些响应内容类型进行修改?
这种事情会变得复杂。在最简单的情况下,您将延迟调用中间件中的服务器 start_response()
,直到您在内存中缓冲了完整的响应,以便您可以修改它并计算内容长度的新响应 header。如果您返回非常大的响应或流式响应,这会导致问题。
如果只处理 HTML 并且只需要在 <head>
中进行更改,那么你可以使用一种缓冲机制,但只缓冲直到它看到 <body>
,或者作为故障保护,缓冲了一定数量的字节。如果您希望在 </body>
之前插入任何内容,那么您将无法避免缓冲所有内容,这通常很糟糕。
最大的问题是你到底想做什么。如果知道这一点,那么可能会提供更好的答案或指导您朝着不同的方向去做。
更新 1
FWIW。如果您使用 mod_wsgi-express,您需要做的就是添加参数为 ssi.conf
的附加 --include-file
选项,并在 ssi.conf
配置文件片段中添加:
LoadModule filter_module ${MOD_WSGI_MODULES_DIRECTORY}/mod_filter.so
LoadModule include_module ${MOD_WSGI_MODULES_DIRECTORY}/mod_include.so
<Location />
Options +Includes
AddOutputFilterByType INCLUDES text/html
</Location>
如果响应内容类型是 text/html
,它将通过 Apache INCLUDES
过滤器并适当扩展。
因此您可以利用:
如果最终目的是在生产中以 Apache 的 SSI 机制为目标,那么这会给你一个更可靠的结果,因为 mod_wsgi-express 仍在使用 Apache 来完成繁重的工作。
好的,我找到了一个解决方案,我没有添加另一个中间件,而是覆盖了 SharedDataMiddleware 并在读取时修改文件。
编辑:添加递归调用以在包含文件中包含文件。
EDIT2:添加了对#echo SSI
的支持
class SharedDataSSIMiddleware(SharedDataMiddleware):
""" Replace SSI includes with the real files on request
"""
ssi_incl_expr = re.compile(r'<!-- *# *include *(virtual|file)=[\'\"]([^\'"]+)[\'\"] *-->')
ssi_echo_expr = re.compile(r'<!-- *# *echo *encoding=[\'\"]([^\'"]+)[\'\"] *var=[\'\"]([^\'"]+)[\'\"] *-->')
def __init__(self, app, exports, disallow=None, cache=True, cache_timeout=60 * 60 * 12, fallback_mimetype='text/plain'):
super(SharedDataSSIMiddleware, self).__init__(app, exports, disallow, cache, cache_timeout, fallback_mimetype)
self.environment = None
def get_included_content(self, path_info, path):
full_path = os.path.join(path_info, path)
with open(full_path) as fp:
data = fp.read()
return self._ssi_include(full_path, data)
def _get_ssi_echo_value(self, encoding, var_name):
return self.environment.get(var_name)
def _ssi_include(self, filename, content):
content = re.sub(
self.ssi_incl_expr,
lambda x: self.get_included_content(os.path.dirname(filename), x.groups()[1]),
content
)
content = re.sub(
self.ssi_echo_expr,
lambda x: self._get_ssi_echo_value(*x.groups()),
content
)
return content
def _opener(self, filename):
file = cStringIO.StringIO()
with open(filename, 'rb') as fp:
content = fp.read()
content = self._ssi_include(filename, content)
file.write(content)
file.flush()
size = file.tell()
file.reset()
return lambda: (file, datetime.utcnow(), size)
def __call__(self, environ, start_response):
self.environment = environ
response = super(SharedDataSSIMiddleware, self).__call__(environ, start_response)
self.environment = None
return response
这会读取实际文件,对其进行修改,然后 returns 一个带有修改后数据的 StringIO 对象,而不是实际文件。
不要在 werkzeug 的 run_simple
中使用 static_files
参数,这只会添加我们在这里不需要的默认 SharedDataMiddleware。
只需使用上面的中间件包装您的应用即可:
app = SharedDataSSIMiddleware(app, exports={'/foo': 'path'})
我正在尝试编写一个中间件来替换响应中的一些数据,从而改变内容长度。对于我们的开发环境,我们想要模拟 SSI 的行为,包括实际网络服务器(如 Nginx 或 Apache)的一些静态文件,这些文件不是通过应用程序提供的。我们正在使用 werkzeug 包含的开发服务器。
这是我目前的情况:
class ModifyBodyMiddleware(object):
def __init__(self, app):
self.app = app
def __call__(self, environment, start_response):
def my_start_response(status, headers, exc_info=None):
# change content-length somehow
start_response(status, headers, exc_info)
body = self.app(environment, my_start_response)
body = do_modifications(body)
return body
为简化起见,假设 do_modifications
确实用 foobar
替换了整个内容。我需要实际的 body 来修改它,但我还需要以某种方式设置新的 content-length header。
谢谢 戈尔
你想修改内容的什么地方?是否应该只对某些响应内容类型进行修改?
这种事情会变得复杂。在最简单的情况下,您将延迟调用中间件中的服务器 start_response()
,直到您在内存中缓冲了完整的响应,以便您可以修改它并计算内容长度的新响应 header。如果您返回非常大的响应或流式响应,这会导致问题。
如果只处理 HTML 并且只需要在 <head>
中进行更改,那么你可以使用一种缓冲机制,但只缓冲直到它看到 <body>
,或者作为故障保护,缓冲了一定数量的字节。如果您希望在 </body>
之前插入任何内容,那么您将无法避免缓冲所有内容,这通常很糟糕。
最大的问题是你到底想做什么。如果知道这一点,那么可能会提供更好的答案或指导您朝着不同的方向去做。
更新 1
FWIW。如果您使用 mod_wsgi-express,您需要做的就是添加参数为 ssi.conf
的附加 --include-file
选项,并在 ssi.conf
配置文件片段中添加:
LoadModule filter_module ${MOD_WSGI_MODULES_DIRECTORY}/mod_filter.so
LoadModule include_module ${MOD_WSGI_MODULES_DIRECTORY}/mod_include.so
<Location />
Options +Includes
AddOutputFilterByType INCLUDES text/html
</Location>
如果响应内容类型是 text/html
,它将通过 Apache INCLUDES
过滤器并适当扩展。
因此您可以利用:
如果最终目的是在生产中以 Apache 的 SSI 机制为目标,那么这会给你一个更可靠的结果,因为 mod_wsgi-express 仍在使用 Apache 来完成繁重的工作。
好的,我找到了一个解决方案,我没有添加另一个中间件,而是覆盖了 SharedDataMiddleware 并在读取时修改文件。
编辑:添加递归调用以在包含文件中包含文件。 EDIT2:添加了对#echo SSI
的支持 class SharedDataSSIMiddleware(SharedDataMiddleware):
""" Replace SSI includes with the real files on request
"""
ssi_incl_expr = re.compile(r'<!-- *# *include *(virtual|file)=[\'\"]([^\'"]+)[\'\"] *-->')
ssi_echo_expr = re.compile(r'<!-- *# *echo *encoding=[\'\"]([^\'"]+)[\'\"] *var=[\'\"]([^\'"]+)[\'\"] *-->')
def __init__(self, app, exports, disallow=None, cache=True, cache_timeout=60 * 60 * 12, fallback_mimetype='text/plain'):
super(SharedDataSSIMiddleware, self).__init__(app, exports, disallow, cache, cache_timeout, fallback_mimetype)
self.environment = None
def get_included_content(self, path_info, path):
full_path = os.path.join(path_info, path)
with open(full_path) as fp:
data = fp.read()
return self._ssi_include(full_path, data)
def _get_ssi_echo_value(self, encoding, var_name):
return self.environment.get(var_name)
def _ssi_include(self, filename, content):
content = re.sub(
self.ssi_incl_expr,
lambda x: self.get_included_content(os.path.dirname(filename), x.groups()[1]),
content
)
content = re.sub(
self.ssi_echo_expr,
lambda x: self._get_ssi_echo_value(*x.groups()),
content
)
return content
def _opener(self, filename):
file = cStringIO.StringIO()
with open(filename, 'rb') as fp:
content = fp.read()
content = self._ssi_include(filename, content)
file.write(content)
file.flush()
size = file.tell()
file.reset()
return lambda: (file, datetime.utcnow(), size)
def __call__(self, environ, start_response):
self.environment = environ
response = super(SharedDataSSIMiddleware, self).__call__(environ, start_response)
self.environment = None
return response
这会读取实际文件,对其进行修改,然后 returns 一个带有修改后数据的 StringIO 对象,而不是实际文件。
不要在 werkzeug 的 run_simple
中使用 static_files
参数,这只会添加我们在这里不需要的默认 SharedDataMiddleware。
只需使用上面的中间件包装您的应用即可:
app = SharedDataSSIMiddleware(app, exports={'/foo': 'path'})