Search code examples
httpgocontent-lengthhttp-content-length

Content-Length header is not getting set for PATCH requests with empty/nil payload - GoLang


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 ?


Solution

  • 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" })