Go的http库处理multipart的两个问题解决

问题说明

最近因为项目需要搞了一些golang的开发,碰到不少问题,除了之前踩的那个关于ActiveMQ的坑以外,最近又在用Gin进行Web开发踩到一个http库里的坑。

Gin本身只是一个简单的包装,大部分功能还是基于net/http这个标准库,其中就包括了Request。

这次碰到的两个问题都是由于一个原因引起的:那就是我在处理POST请求接收文件上传的时候,有时会丢失请求参数(上传文件可以收到,但一起传过来的Post参数为空)。

经过返复测试,发现用python的requests发过来的请求是可以正常处理的,但是用Java发过来的请求就出错。

为了调试这个问题,我想把请求内容打出日志来,于是碰到了第一个问题:

Request.Body是一个ReaderCloser缓冲,只能读一次,如果我读出来打日志,那么后面的参数肯定都为空,如果取了参数,则这个缓冲也为空。

当然,第二个问题就是那个POST请求丢失的问题,其原因后面说。

无损读取Request.Body的方法

按官方文档和Stackoverflow上找来的各自方法都尝试过,比如读完再写回,或者是用Bind,都不能正常取到内容,最后是在一个外国人的BLOG的评论里看到有人提到一个方法:

httputil.DumpRequest

最终解决了这个问题,写了一个调试专用的方法:

func req_body(c *gin.Context) string {
    body, err := httputil.DumpRequest(c.Request, true)
    if err != nil {
        println(err)
    }
    return string(body[:1024])
}

POST参数丢失问题

有了上面这个方法,我终于找到问题的所在了。下面分别是python和Java的请求内容,注意其中的区别:

python:

Connection: close
Accept: */*
Accept-Encoding: gzip, deflate
Connection: close
Content-Length: xxx
Content-Type: multipart/form-data; boundary=5b9ca15e3fd14316b6a4b03cb4ee4de2
User-Agent: python-requests/2.12.3
X-Forwarded-For: 120.36.xx
X-Real-Ip: 120.36.xx

--5b9ca15e3fd14316b6a4b03cb4ee4de2
Content-Disposition: form-data; name="index"

1
--5b9ca15e3fd14316b6a4b03cb4ee4de2
Content-Disposition: form-data; name="checksum"

testchecksum

Java:

Connection: close
Connection: close
Content-Length: xxx
Content-Type: multipart/form-data; boundary=JqsRkZ2MjzUUb4YOaDn-FvuI_vsjn0sXhhZZ
d
User-Agent: Apache-HttpClient/4.5.2 (Java/1.7.0_79)
X-Forwarded-For: 1.192.xx
X-Real-Ip: 1.192.xx

--JqsRkZ2MjzUUb4YOaDn-FvuI_vsjn0sXhhZZd
Content-Disposition: form-data; name="index"
Content-Type: text/plain; charset=ISO-8859-1
Content-Transfer-Encoding: 8bit

2
--JqsRkZ2MjzUUb4YOaDn-FvuI_vsjn0sXhhZZd
Content-Disposition: form-data; name="checksum"
Content-Type: text/plain; charset=ISO-8859-1
Content-Transfer-Encoding: 8bit

testchecksum

显然,Java版的每个part头里多了一些东西,应该就是这些东西导致了http对multipart的解析失败。

但是我反复试了PostForm和MultipartForm,都无法取得POST参数——PostForm取到的都是空,而MultipartForm取得的form.Value也是空。

最后是我偶然想到试试看MultipartForm的form.File,结果发现原来参数都被解析到这里了……真不知道这个http标准库是怎么设计的,难道这么多Go用户都没有发现吗?还是说Go的用户实际上并没有那么多?

总之为了解决这个问题,只好自己写了一点代码来把File里的参数转到Value里去:

func req_getpart(v *multipart.FileHeader) (string, error) {
    f, err := v.Open()
    if err != nil {
        println("Open fail")
        return "", err
    }
    buf, err := ioutil.ReadAll(f)
    defer f.Close()
    if err != nil {
        println("Read fail")
        return "", err
    }
    return string(buf), nil
}

func req_multipart(c *gin.Context) *multipart.Form {
    form, _ := c.MultipartForm()
    if len(form.Value) == 0 && len(form.File) > 0 {
        for k, v := range form.File {
            if len(v) > 0 {
                buf, err := req_getpart(v[0])
                if err != nil {
                    continue
                }
                form.Value[k] = append(form.Value[k], buf)
            }
        }
        for k, _ := range form.Value {
            delete(form.File, k)
        }
    }
    return form
}

推送到[go4pro.org]