Search code examples
gonetwork-programminghttprequesthttpclient

GoLang - follow redirect for POST requests with body data


I want to follow redirect with the same body for POST request.

From GO sources (client.go)

func redirectBehavior(reqMethod string, resp *Response, ireq *Request) (redirectMethod string, shouldRedirect, includeBody bool) {
    switch resp.StatusCode {
    case 301, 302, 303:
        redirectMethod = reqMethod
        shouldRedirect = true
        includeBody = false

        // RFC 2616 allowed automatic redirection only with GET and
        // HEAD requests. RFC 7231 lifts this restriction, but we still
        // restrict other methods to GET to maintain compatibility.
        // See Issue 18570.

But sometimes server returns 302 Redirect for POST request that means I need to send the same body to another location.

What should I do in this situation?

func FollowRedirectForPost() {
    client := &http.Client{}
    
    req, _ := http.NewRequest(http.MethodPost, "example.com/test", strings.NewReader(url.Values{
        "key": {"value"},
        "key1":{"value1"},
    }.Encode()))

    req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
    client.Do(req) // If server returns 302 Redirect so it means I need make the same request with the same body to
    // a different location. Just imagine i replaced "example.com/test" to "example.com/redirect_url"
}

Solution

  • From RFC7231:

    The server SHOULD generate a Location header field in the response containing a URI reference for the different URI. The user agent MAY use the Location field value for automatic redirection. The server's response payload usually contains a short hypertext note with a hyperlink to the different URI(s).

    Note: For historical reasons, a user agent MAY change the request method from POST to GET for the subsequent request. If this behavior is undesired, the 307 (Temporary Redirect) status code can be used instead.

    So you MAY follow the redirect, the new URI is in the Location header. You don't have to. And you MAY change the the method to GET, but don't have to. So essentially, anything you do is RFC compliant.

    You can provide your own redirect policy by supplying a CheckRedirect function. redirectPostOn302 basically does the same as the client would do if includeBody was true and redirectMethod was POST:

    func FollowRedirectForPost() {
        client := &http.Client{
            CheckRedirect: redirectPostOn302,
        }
        
        req, _ := http.NewRequest(http.MethodPost, "example.com/test", strings.NewReader(url.Values{
            "key": {"value"},
            "key1":{"value1"},
        }.Encode()))
    
        req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
        client.Do(req) // If server returns 302 Redirect so it means I need make the same request with the same body to
        // a different location. Just imagine i replaced "example.com/test" to "example.com/redirect_url"
    }
    
    func redirectPostOn302(req *http.Request, via []*http.Request) error {
        if len(via) >= 10 {
            return errors.New("stopped after 10 redirects")
        }
    
        lastReq := via[len(via)-1]
        if req.Response.StatusCode == 302 && lastReq.Method == http.MethodPost {
            req.Method = http.MethodPost
    
            // Get the body of the original request, set here, since req.Body will be nil if a 302 was returned
            if via[0].GetBody != nil {
                var err error
                req.Body, err = via[0].GetBody()
                if err != nil {
                    return err
                }
                req.ContentLength = via[0].ContentLength
            }
        }
    
        return nil
    }