3

I observed that Content-Length header is not getting set for PATCH requests with empty/nil payload. Even if we manually set it by req.Header.Set("content-length", "0") it is not actually getting set in the out going request. This strange behaviour (Go bug?) happens only for PATCH requests and only when the payload is empty or nil (or set to http.NoBody)

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "strings"
)

func main() {

    url := "http://localhost:9999"
    method := "PATCH"

    payload := strings.NewReader("")
    client := &http.Client {
    }
    req, err := http.NewRequest(method, url, payload)

    if err != nil {
        fmt.Println(err)
    }
    req.Header.Set("Authorization", "Bearer my-token")
    req.Header.Set("Content-Length", "0") //this is not honoured

    res, err := client.Do(req)
    defer res.Body.Close()
    body, err := ioutil.ReadAll(res.Body)

    fmt.Println(string(body))
}

This is reproducible even in the latest go version 1.15. Just run the above code against a simple http server and see for yourself.

Is there any solution/workaround to send a PATCH request with Content-Length set to 0 ?

tharinduwijewardane
  • 2,593
  • 2
  • 16
  • 28
  • Absence of `Content-Length` is also acceptable to indicate a nil body. This is not a client bug, it's a server bug if it's not being handled properly. – Marc Aug 22 '20 at 15:32
  • See also https://stackoverflow.com/questions/33005158/which-method-needs-content-length-field – Marc Aug 22 '20 at 15:33
  • @Marc Azure data lake gen2 storage API is throwing errors for this https://learn.microsoft.com/en-us/rest/api/storageservices/datalakestoragegen2/path/update – tharinduwijewardane Aug 22 '20 at 15:35
  • Furthermore, the [docs](https://golang.org/pkg/net/http/#Request) clearly state that the header might be ignored. – Marc Aug 22 '20 at 15:35
  • Then they have a bug in their handler. Imagine that, Microsoft having a hard time reading an RFC. – Marc Aug 22 '20 at 15:36
  • I will report this to them, but It may take a long time for them to fix this. At the meantime is there any workaround you can think of ? – tharinduwijewardane Aug 22 '20 at 15:39
  • Sorry, I know how frustrating this can be (different RFC, similar problem). I can dig a bit more when I'm back at a computer. Maybe try looking for alternatives to the API or using PATCH? – Marc Aug 22 '20 at 16:27
  • thanks, using gorilla/http instead of net/http we can manually set the content-length header but the issue is gorilla/http does not support oauth2 :( – tharinduwijewardane Aug 22 '20 at 16:50

1 Answers1

1

You can tell the HTTP client to include a Content-Length header with value 0 by setting TransferEncoding to identity as follows:

  url := "http://localhost:9999"
  method := "PATCH"
  
  client := &http.Client{}
  req, err := http.NewRequest(method, url, http.NoBody)
  if err != nil {
    panic(err)
  } 

  req.TransferEncoding = []string{"identity"} 
  req.Header.Set("Authorization", "Bearer my-token")
  //  req.Header.Set("Content-Length", "0")

Note the following changes to your original code:

  • the important one: req.TransferEncoding = []string{"identity"}
  • the idiomatic way of specifying an empty body: http.NoBody (no impact on sending the length)
  • commented out req.Header.Set("Content-Length", "0"), the client fills it in by itself
  • also changed to panic on an error, you probably don't want to continue

The transfer encoding of identity is not written to the request, so except for the header Content-Length = 0, the request looks the same as before.

This is unfortunately not documented (feel free to file an issue with the Go team), but can be seen in the following code:

The tedious details:

transferWriter.writeHeader checks the following to write the Content-Length header:

    // Write Content-Length and/or Transfer-Encoding whose values are a
    // function of the sanitized field triple (Body, ContentLength,
    // TransferEncoding)
    if t.shouldSendContentLength() {
        if _, err := io.WriteString(w, "Content-Length: "); err != nil {
            return err
        }
        if _, err := io.WriteString(w, strconv.FormatInt(t.ContentLength, 10)+"\r\n"); err != nil {
            return err
        }

In turn, shouldCheckContentLength looks at the transfer encoding in case of zero length:

    if t.ContentLength == 0 && isIdentity(t.TransferEncoding) {
        if t.Method == "GET" || t.Method == "HEAD" {
            return false
        }
        return true
    }

The isIdentity verifies that TransferEncoding is exactly []string{"identity"}:

func isIdentity(te []string) bool { return len(te) == 1 && te[0] == "identity" }) 
Marc
  • 19,394
  • 6
  • 47
  • 51