• 解决 php post 而 gin 收不到问题


    缘由

    php 这边用 post+json 形式向 golang 的 gin 框架发送数据,之前网页版都是使用正常,最近有个需求是一个功能接入移动端内部办公系统,通过接入 sdk 调用实现网页上的功能。

    现象

    php 作为业务代理层,会接收客户端传过来的数据,然后通过封装的 curl 库发送给 golang 微服务。但是这次在接收到 sdk 的发过来的数据,组装加工并转发到 gin 时,却一直报错。

     "Key: 'SubmitReq.TaskID' Error:Field validation for 'TaskID' failed on the 'required' tag
     
    Key: 'SubmitReq.ResultStatus' Error:Field validation for 'ResultStatus' failed on the 'required' tag"
    
    • 1
    • 2
    • 3

    熟悉 gin 的童鞋就应该知道,这是 shouldBind 报出的,也就是 SubmitReq 这个结构里有两个必传字段,但是在接收时,却没有收到,因此报错。

    type SubmitReq struct {
    	TaskID       string                 `form:"taskId" json:"taskId" binding:"required"`
    	ResultStatus int64                  `form:"resultStatus" json:"resultStatus" binding:"required"`
    }
    
    var submitReq structure.submitReq
    if err := c.ShouldBind(&submitReq); err != nil {
    	rsp.Errno = config.ErrnoParams
    	rsp.Errmsg = err.Error()
    	c.JSON(http.StatusOK, rsp)
    	return
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    解决

    首先是完善 gin 入口那的接收 http 请求日志

    log.Infof("_com_request_in||%v||from=%v||uri=%v||url=%v||method=%v||contentType=%v||header=%+v||args=%v", traceInfo, c.ClientIP(), c.Request.URL.Path, c.Request.URL, c.Request.Method, c.Request.Header.Get("Content-Type"), c.Request.Header, string(data))
    
    • 1

    接下来就是模拟发送了,并通过三次发送,定位到了问题。

    最开始,模拟实际情况,先到 php 服务上找到当时 sdk 发过来的日志,然后拿到发送的数据,模拟发送,在 gin 服务上发现日志中的 method 为 post,而 contentType 和 args 都为空,这就很有问题。

    翻了翻源码,发现 contentType 为空时,会按照下载逻辑处理。实际上,就是不处理参数,接收值为空,也就报上述的错误了。

    func parsePostForm(r *Request) (vs url.Values, err error) {
    	if r.Body == nil {
    		err = errors.New("missing form body")
    		return
    	}
    	ct := r.Header.Get("Content-Type")
    	// RFC 7231, section 3.1.1.5 - empty type
    	//   MAY be treated as application/octet-stream
    	if ct == "" {
    		ct = "application/octet-stream"
    	}
    	ct, _, err = mime.ParseMediaType(ct)
    	switch {
    	case ct == "application/x-www-form-urlencoded":
    		var reader io.Reader = r.Body
    		maxFormSize := int64(1<<63 - 1)
    		if _, ok := r.Body.(*maxBytesReader); !ok {
    			maxFormSize = int64(10 << 20) // 10 MB is a lot of text.
    			reader = io.LimitReader(r.Body, maxFormSize+1)
    		}
    		b, e := io.ReadAll(reader)
    		if e != nil {
    			if err == nil {
    				err = e
    			}
    			break
    		}
    		if int64(len(b)) > maxFormSize {
    			err = errors.New("http: POST too large")
    			return
    		}
    		vs, e = url.ParseQuery(string(b))
    		if err == nil {
    			err = e
    		}
    	case ct == "multipart/form-data":
    		// handled by ParseMultipartForm (which is calling us, or should be)
    		// TODO(bradfitz): there are too many possible
    		// orders to call too many functions here.
    		// Clean this up and write more tests.
    		// request_test.go contains the start of this,
    		// in TestParseMultipartFormOrder and others.
    	}
    	return
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    第二次,找到 php 中 curl 填充 header 的 Content-Type 处的代码,发现有逻辑漏洞

    if (!empty($userInfo)) {
        $headerContentType = !empty($contentType) ? $contentType : $_SERVER['HTTP_CONTENT_TYPE'];
        $ctx->extHeaders = ["userinfo: " . json_encode($userInfo, JSON_UNESCAPED_UNICODE), "Content-Type: " . $headerContentType];
    }
    
    • 1
    • 2
    • 3
    • 4

    如果上游接口中,传了 $userInfo 但是 $contentType 为空,这时 $_SERVER['HTTP_CONTENT_TYPE'] 若为空,就是最开始时的错误。后来通过打印 php 的日志也发现了这点,Content-Type 确实为空。

    于是,补充了下相关逻辑,加上了 application/json; charset=utf-8 ,并发送了模拟请求。不过依然是第一次报的错误。

    第三次,仔细看了下 gin 打印的日志,终于发现了端倪,在 method 是 POST,Content-Type 是 application/json; charset=utf-8 情况下,参数 args 为

    taskId=1&resultStatus=1
    
    • 1

    本来应该要传 json 格式的,可传的是 form 形式的(也可以说是 queryString 的)。虽然, gin 支持 json、form 和 queryString 三种格式数据解析,但是在 method 和 header 头类型都确定的情况下,没传 json ,而是其他格式的,难怪会解析报错。

    func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
    	if req == nil || req.Body == nil {
    		return fmt.Errorf("invalid request")
    	}
    	return decodeJSON(req.Body, obj)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    问题,还是出现在 php 封装的 curl 代码那

    curl_setopt($ci, CURLOPT_POSTFIELDS, is_string($params) ? $params : http_build_query($params));
    
    • 1

    原来最后发送数据这里,是不管前面设置啥 Content-Type 的,由于前面传的是数组,这里肯定会以 form 形式传过去的。

    解决,也很简单,就是在上游收敛了传的 Content-Type 和 params 数据,这样就不会出现挂羊头卖狗肉的情形了。

  • 相关阅读:
    C++之多态<polymorphism>
    10.DesignForSymbols\AddLibTextLabelsAll
    POJ1007:DNA排序
    useEffect(fn, []) 不等于 componentDidMount()
    Linux入门攻坚——3、基础命令学习-文件管理、别名、glob、重定向、管道、用户及组管理、权限管理
    127. 单词接龙
    Spring Data JPA
    上海亚商投顾:沪指低开低走 抖音概念股逆势爆发
    D. Yet Another Sorting Problem
    自动驾驶中的坐标系变换
  • 原文地址:https://blog.csdn.net/molaifeng/article/details/128178486