如何在 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'})