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

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

内容简介:这个其实是我的锅,不过我还是想"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问题

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

查看所有标签

猜你喜欢:

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

学习JavaScript数据结构与算法(第2版)

学习JavaScript数据结构与算法(第2版)

[巴西] Loiane Groner / 邓 钢、孙晓博、吴 双、陈 迪、袁 源 / 人民邮电出版社 / 2017-9 / 49.00元

本书首先介绍了JavaScript 语言的基础知识以及ES6 和ES7 中引入的新功能,接下来讨论了数组、栈、队列、链表、集合、字典、散列表、树、图等数据结构,之后探讨了各种排序和搜索算法,包括冒泡排序、选择排序、插入排序、归并排序、快速排序、堆排序、计数排序、桶排序、基数排序、顺序搜索、二分搜索,然后介绍了动态规划和贪心算法等常用的高级算法以及函数式编程,最后还介绍了如何计算算法的复杂度。一起来看看 《学习JavaScript数据结构与算法(第2版)》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

在线进制转换器
在线进制转换器

各进制数互转换器

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

HSV CMYK互换工具