使用Django+channels+Python3.7时提交Form表单: 400 Bad Request问题

栏目: Python · 发布时间: 7年前

内容简介:这个其实是我的锅,不过我还是想"Blame"那个吞噬异常的程序员。上次在自己的博客项目上尝试了Python3.7的beta版之后,意识到Celery因为惯性还是不能兼容3.7,所以不在做升级的打算。直到前不久开始弄一个简单的内部社区,针对购买视频的同学。这也是个人项目,所以激进点没什么关系。既然是尝鲜,那就顺便也尝尝Django的channels,用它的Websocket来做桌面通知,也就是Chrome提供的:Notifications API 。

不太好升级的 Python 3.7之二

这个其实是我的锅,不过我还是想"Blame"那个吞噬异常的程序员。

上次在自己的博客项目上尝试了Python3.7的beta版之后,意识到Celery因为惯性还是不能兼容3.7,所以不在做升级的打算。直到前不久开始弄一个简单的内部社区,针对购买视频的同学。这也是个人项目,所以激进点没什么关系。

既然是尝鲜,那就顺便也尝尝Django的channels,用它的Websocket来做桌面通知,也就是Chrome提供的:Notifications API 。

一开始的Python版本是3.6,开发部署都没问题,功能也没问题。在部署后想到,不如试试3.7。虽然channels的包声明上还没说能够兼容3.7。

安装3.7的过程也不顺利,这篇暂且按下不表。

安装好3.7之后,部署流程没什么差别,毕竟编写好的fabric脚本,只是把创建虚拟环境的命令改为了: python3.7 -m venv {project}

单说问题表现吧,或许你也可能遇到:通过Ajax发送的post请求,后端可以正常处理,但是通过Form表单提交的POST请求一律 400 Bad Request

排查问题

首先需要确认的是请求有没有打到后端upstream上,通过排查Openresty的日志发现,是后端响应的400,那么接下来就应该去排查应用了。

好戏才刚刚开始。

按照往常的部署方式:Gunicorn + gthread + Django WSGI,要调试这样的问题并不困难,因为一直在用,所以偶尔会看下源码。但问题是我使用了channels,所以部署的方式就变为了:Daphne + Django ASGI了。(这里说一下,有一个uvicorn的ASGI容器的实现,性能压测表现也很棒,只是不能用supervisord来重启,所以就使用channels推荐的Daphne了)

在现在的情况下要调试就不太容易了。为啥呢?

channels依赖daphne,而daphne依赖twisted。对外的接口是异步的逻辑,所以调试起来没那么容易。

因为是Django的项目,所以要确认是否有请求过来,首先要做的是在view里加日志,没有收到请求。接着在Middleware中增加日志,还是没有请求。

这意味着什么?请求没有进入Middleware的处理逻辑,也就是WSGI情况下对WSGIHandler的 call 的调用。

我对asgi的逻辑目前还不是特别清楚 ,单从代码上看ASGI和WSGI也差不多。对于http的请求,它使用的是ASGIHandler来处理,依然是继承自Django的 core.handlers.base.BaseHandler (WSGIHandler也是继承自它)。

不过在这里调试依然没有收获,这说明请求的数据根本没到达Handler的部分,那就应该是再往前一层的逻辑了,处理HTTP协议部分的逻辑。如果是Gunicorn + gthread的方式,直接去看对应的socket处理代码就好。不过channels前面Daphne的Server,Daphne Server中用的是 twisted.web.http 下的 HTTPFactory 来封装HTTP协议,而在HTTPFactory中,用的是 twisted.web.http.Request 来处理HTTP协议。

说到这,坑就来了。

不过我的具体定位的方法没有那么复杂,毕竟在熬夜的情况下要把代码都读一下也挺耗时间的。所以直接搜索 400 Bad Request 或者 400 关键字,在twisted和daphne的代码中。最终也是定位到了 twsited.web.http.Request 中。

具体的代码如下:

@implementer(interfaces.IConsumer,
             _IDeprecatedHTTPChannelToRequestInterface)
class Request:

    def requestReceived(self, command, path, version):
        """
        Called by channel when all data has been received.

        This method is not intended for users.

        @type command: C{bytes}
        @param command: The HTTP verb of this request.  This has the case
            supplied by the client (eg, it maybe "get" rather than "GET").

        @type path: C{bytes}
        @param path: The URI of this request.

        @type version: C{bytes}
        @param version: The HTTP version of this request.
        """
        self.content.seek(0,0)
        self.args = {}

        self.method, self.uri = command, path
        self.clientproto = version
        x = self.uri.split(b'?', 1)

        if len(x) == 1:
            self.path = self.uri
        else:
            self.path, argstring = x
            self.args = parse_qs(argstring, 1)

        # Argument processing
        args = self.args
        ctype = self.requestHeaders.getRawHeaders(b'content-type')
        if ctype is not None:
            ctype = ctype[0]

        if self.method == b"POST" and ctype:
            mfd = b'multipart/form-data'
            key, pdict = _parseHeader(ctype)
            if key == b'application/x-www-form-urlencoded':
                args.update(parse_qs(self.content.read(), 1))
            elif key == mfd:
                try:
                    # print(pdict)
                    # import pdb;pdb.set_trace()
                    cgiArgs = cgi.parse_multipart(self.content, pdict)

                    if _PY3:
                        # parse_multipart on Python 3 decodes the header bytes
                        # as iso-8859-1 and returns a str key -- we want bytes
                        # so encode it back
                        self.args.update({x.encode('iso-8859-1'): y
                                          for x, y in cgiArgs.items()})
                    else:
                        self.args.update(cgiArgs)
                except:  # 注意:坑在这
                    # It was a bad request.
                    self.channel._respondToBadRequestAndDisconnect()
                    return
            self.content.seek(0, 0)

        self.process()

上面的 self.channel._respondToBadRequestAndDisconnect 代码如下,通过关键字搜索先找到这块代码。

def _respondToBadRequestAndDisconnect(self):
    """
    This is a quick and dirty way of responding to bad requests.

    As described by HTTP standard we should be patient and accept the
    whole request from the client before sending a polite bad request
    response, even in the case when clients send tons of data.

    @param transport: Transport handling connection to the client.
    @type transport: L{interfaces.ITransport}
    """
    self.transport.write(b"HTTP/1.1 400 Bad Request\r\n\r\n")
    self.loseConnection()

问题总结

到了具体代码的位置,问题也就明了了。总结下原因。

在Python3.7的changelog里面:https://docs.python.org/3.7/whatsnew/changelog.html#changelog

bpo-33497: Add errors param to cgi.parse_multipart and make an encoding in FieldStorage use the given errors (needed for Twisted). Patch by Amber Brown. bpo-29979: rewrite cgi.parse_multipart, reusing the FieldStorage class and making its results consistent with those of FieldStorage for multipart/form-data requests. Patch by Pierre Quentel.

去看对应的pull request会发现, cgi.parse_multipart 被重写了,强制需要 CONTENT-LENGTH :

headers['Content-Length'] = pdict['CONTENT-LENGTH']

而我上面贴出来的代码,其中调用 cgi.parse_multipart 方法的部分,外层有一个宽泛的异常处理,并且没输出任何日志。当然也因为传进去的参数有问题。

知道了问题所以就去看了眼twisted在GitHub上的代码,竟然已经处理了。(顺便提一下,那个吞掉异常的代码就是Amber Brown 2015年写的,后来也是她解决的。看twisted的commit,很多她的提交。并且最近的一些Release都是她主导的。我只能说,谁年轻时还不写几个糟糕的代码呢。不过新的代码依然没有输出日志啊, -.-| )

终极原因

上面说了一大堆内容,终极的原因其实是我用了一个老的twisted包(18.4.0),最新的是18.7.0。

总结

  • 宽泛的异常捕获,并且不做任何输出,简直就是大坑。
  • 尝鲜的情况下,最好都用新的版本,避免出现上面的问题。
  • channels跟Django结合的很好,用起来顺手,调试起来麻烦。
  • 有空应该看看twisted,毕竟channels用到了它。

参考

  • https://reinout.vanrees.org/weblog/2015/11/06/twisted-and-django.html
  • https://labs.twistedmatrix.com/
  • https://github.com/twisted/twisted/blob/trunk/src/twisted/web/http.py
  • https://github.com/python/cpython/pull/991/files

- from the5fire.com

----EOF-----

微信公众号:Python程序员杂谈

使用Django+channels+Python3.7时提交Form表单: 400 Bad Request问题

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

软件框架设计的艺术

软件框架设计的艺术

[捷] Jaroslav Tulach / 王磊、朱兴 / 人民邮电出版社 / 2011-3 / 75.00元

本书帮助你解决API 设计方面的问题,共分3 个部分,分别指出学习API 设计是需要进行科学的训练的、Java 语言在设计方面的理论及设计和维护API 时的常见情况,并提供了各种技巧来解决相应的问题。 本书作者是NetBeans 的创始人,也是NetBeans 项目最初的架构师。相信在API 设计中遇到问题时,本书将不可或缺。 本书适用于软件设计人员阅读。一起来看看 《软件框架设计的艺术》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

html转js在线工具
html转js在线工具

html转js在线工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具