Search code examples
gogo-gin

Unit testing of gin's Context.Redirect works for GET response code but fails for POST response code (golang)


I want my server to redirect a particular end point to another server. This end point can be either GETted or POSTed. The HTTP response code should be 302 in both cases. And if I use curl on this code, it does indeed show response code 302 in both cases, and curl -L follows the redirect properly. Whew.

BUT

my unit test uses the httptest.NewRecorder() to capture the information, but it only works for GET and not for POST. So I need to figure out how to get the unit test to work, when I know that the actual redirect is working. The fail test shows that the HTTP response code is 200 instead of 302 (http.StatusFound).

$ go run foo.go
POST code 200
GET code 302

Here's the self contained test.

package main

import (
    "net/http"
    "net/http/httptest"
    "github.com/gin-gonic/gin"
)

func main() {
    gin.SetMode(gin.ReleaseMode)
    {
        w := httptest.NewRecorder()
        context, _ := gin.CreateTestContext(w)
        context.Request = httptest.NewRequest("POST", "http://localhost:23632/foobar", nil)
        context.Redirect(http.StatusFound, "http://foobar.com")

        print("POST code ",w.Code,"\n")
    }

    {
        w := httptest.NewRecorder()
        context, _ := gin.CreateTestContext(w)
        context.Request = httptest.NewRequest("GET", "http://localhost:23632/foobar", nil)
        context.Redirect(http.StatusFound, "http://foobar.com")

        print("GET code ",w.Code,"\n")
    }
}

When I do CURL POST on the actual app (not shown), I see that it is working:

curl -v -XPOST localhost:23632/foobar
* About to connect() to localhost port 23632 (#0)
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 23632 (#0)
> POST /foobar HTTP/1.1
> User-Agent: curl/7.29.0
> Host: localhost:23632
> Accept: */*
>
< HTTP/1.1 302 Found
< Location: http://foobar.com
< Vary: Origin
< Date: Tue, 23 May 2023 22:38:42 GMT
< Content-Length: 0
<
* Connection #0 to host localhost left intact

Solution

  • TL;DR

    The workaround is to call context.Writer.WriteHeaderNow explicitly after context.Redirect.

    Explanation

    This is a corner case of using the gin context returned from gin.CreateTestContext.

    For the GET request, gin will call http.Redirect finally, which will write a short HTML body (something like <a href="http://foobar.com">Found</a>) to the response, which in turn causes the status code to be written to the response.

    While for the POST request, http.Redirect does not write the short HTML body, and the status code don't have the chance to be written to the response.

    See the implementation of http.Redirect. According to the source code, if the header Content-Type is set before, the GET request will have the same issue too:

      {
        w := httptest.NewRecorder()
        context, _ := gin.CreateTestContext(w)
        context.Request = httptest.NewRequest("GET", "http://localhost:23632/foobar", nil)
    +   context.Header("Content-Type", "text/html")
        context.Redirect(http.StatusFound, "http://foobar.com")
        print("GET code ", w.Code, "\n")
      }
    

    The workaround is to call context.Writer.WriteHeaderNow explicitly:

      {
        w := httptest.NewRecorder()
        context, _ := gin.CreateTestContext(w)
        context.Request = httptest.NewRequest("POST", "http://localhost:23632/foobar", nil)
        context.Redirect(http.StatusFound, "http://foobar.com")
    +   context.Writer.WriteHeaderNow()
    
        print("POST code ", w.Code, "\n")
      }
    

    gin itself uses the same workaround. See TestContextRenderRedirectWithRelativePath.

    A real server app does not suffer from the same issue because (*Engine).handleHTTPRequest will call WriteHeaderNow for us (see the source code). That's why I call it a corner case instead of a bug.