从 bufio.Reader 创建请求

creating a request from bufio.Reader

我正在尝试实现一个接受多部分混合的批处理程序。

我目前有点幼稚的实现如下所示。稍后我会尝试聚合响应并发送多部分响应。

我目前的问题是无法将各个部分的 body 解析为新请求。

func handleBatchPost(w http.ResponseWriter, r *http.Request) {
  // read the multipart body
  reader, err := r.MultipartReader()
  if err != nil {
    http.Error(w, fmt.Sprintf("could not read multipart %v\n", err), http.StatusBadRequest)
  }

  // read each part
  for {
    part, err := reader.NextPart()
    if err == io.EOF {
      break
    } else if err != nil {
      http.Error(w, fmt.Sprintf("could not read next part %v\n", err), http.StatusBadRequest)
      return
    }

    // check if content type is http
    if part.Header.Get("Content-Type") != "application/http" {
      http.Error(w, fmt.Sprintf("part has wrong content type: %s\n", part.Header.Get("Content-Type")), http.StatusBadRequest)
      return
    }

    // parse the body of the part into a request
    req, err := http.ReadRequest(bufio.NewReader(part))
    if err != nil {
      http.Error(w, fmt.Sprintf("could not create request: %s\n", err), http.StatusBadRequest)
      return
    }

    // handle the request
    router.ServeHTTP(w, req)
  }
}

func handleItemPost(w http.ResponseWriter, r *http.Request) {
  var item map[string]interface{}
  if err := json.NewDecoder(r.Body).Decode(&item); err != nil {
    http.Error(w, fmt.Sprintf("could not decode item json: %v\n", err), http.StatusBadRequest)
    return
  }
  w.Write([]byte(`{"success": true}`))
}

我收到来自服务器的错误响应。似乎 ReadRequest 没有阅读 body 而只是 headers(方法、url 等)。

could not decode item json: EOF

这是我发送的负载。

POST /batch  HTTP/1.1
Host: localhost:8080
Content-Type: multipart/mixed; boundary=boundary

--boundary
Content-Type: application/http
Content-ID: <item1>

POST /items HTTP/1.1
Content-Type: application/json

{ "name": "batch1", "description": "batch1 description" }

--boundary
Content-Type: application/http
Content-ID: <item2>

POST /items HTTP/1.1
Content-Type: application/json

{ "name": "batch2", "description": "batch2 description" }

--boundary--

我在 gmail api 文档 https://developers.google.com/gmail/api/guides/batch 上找到了这个模式。

主要问题是您的负载没有为 sub-requests 指定 Content-Length header。如果缺少 Content-Length header,http.ReadRequest() 将假定没有 body,将不会读取和显示实际的 body,这就是为什么会出现 EOF 错误.

所以首先提供缺失的Content-Length headers:

POST /batch  HTTP/1.1
Host: localhost:8080
Content-Type: multipart/mixed; boundary=boundary

--boundary
Content-Type: application/http
Content-ID: <item1>

POST /items HTTP/1.1
Content-Type: application/json
Content-length: 58

{ "name": "batch1", "description": "batch1 description" }

--boundary
Content-Type: application/http
Content-ID: <item2>

POST /items HTTP/1.1
Content-Type: application/json
Content-length: 58

{ "name": "batch2", "description": "batch2 description" }

--boundary--

这样它应该可以工作,但请注意,由于您在同一个循环中处理零件,并在最后调用 router.ServeHTTP(w, req),因此您 重用了 w作家。这是什么意思?如果 handleItemPost() 向输出写入任何内容,则对 handleItemPost() 的后续调用无法收回。

例如如果 handleItemPost() 失败,它会以 HTTP 错误响应(这意味着设置响应状态并写入 body)。后续的handleItemPost()不能再报错(headers已经commit),而且如果报成功,错误header已经发送,只能写进一步的消息到错误 body.

例如,如果我们将 handleItemPost() 修改为:

func handleItemPost(w http.ResponseWriter, r *http.Request) {
    var item map[string]interface{}
    if err := json.NewDecoder(r.Body).Decode(&item); err != nil {
        fmt.Printf("JSON decode error: %v\n", err)
        return
    }
    fmt.Printf("Success, item: %v\n", item)
}

并执行以下 curl 命令:

curl localhost:8080/batch -X POST \
    -H "Content-Type: multipart/mixed; boundary=boundary" \
    -d '--boundary
Content-Type: application/http
Content-ID: <item1>

POST /items HTTP/1.1
Content-Type: application/json
Content-length: 58

{ "name": "batch1", "description": "batch1 description" }

--boundary
Content-Type: application/http
Content-ID: <item2>

POST /items HTTP/1.1
Content-Type: application/json
Content-length: 58

{ "name": "batch2", "description": "batch2 description" }

--boundary--'

我们将看到以下输出:

Success, item: map[description:batch1 description name:batch1]
Success, item: map[description:batch2 description name:batch2]

请注意,如果 handleItemPost() 需要保持完整的功能并可自行调用(以处理请求并产生响应),则不能对它的所有调用使用相同的 http.ResponseWriter .

在这种情况下,您可以为每个调用创建和使用单独的 http.ResponseWriter。标准库有一个实现 http.ResponseWriterhttptest.ResponseRecorder 类型。它主要用于测试目的,但您也可以在这里使用它。它记录了书面答复,因此您可以在通话后查看它。

例如:

w2 := httptest.NewRecorder()
router.ServeHTTP(w2, req)
if w2.Code != http.StatusOK {
    fmt.Printf("handleItemPost returned non-OK status: %v\n", w2.Code)
    fmt.Printf("\terror body: %v\n", w2.Body.String())
}

运行 这与您的原始请求(未指定 Content-Length),输出将是:

handleItemPost returned non-OK status: 400
        error body: could not decode item json: EOF


handleItemPost returned non-OK status: 400
        error body: could not decode item json: EOF

但是当你指定sub-requests的Content-Length时,没有打印输出(错误)。