gin-gonic / gin

Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API with much better performance -- up to 40 times faster. If you need smashing performance, get yourself some Gin.
https://gin-gonic.com/
MIT License
77.91k stars 7.97k forks source link

timestamps escaped differently in go 1.20 #3507

Open sobafuchs opened 1 year ago

sobafuchs commented 1 year ago

Description

I've discovered that timestamps are escaped differently in Go 1.20. This may not be a huge issue, but if you do snapshot testing and test against the validation errors from gin, it will cause them to fail when comparing payloads between Go 1.19 and Go 1.20 because timestamps used to have extra escaping that now have disappeared in Go 1.20. Was this intentional or am I doing something weird here?

How to reproduce

go.mod

module gin-bug

go 1.20

require (
    github.com/bradleyjkemp/cupaloy/v2 v2.8.0 // indirect
    github.com/davecgh/go-spew v1.1.1 // indirect
    github.com/gin-contrib/sse v0.1.0 // indirect
    github.com/gin-gonic/gin v1.8.2 // indirect
    github.com/go-playground/locales v0.14.1 // indirect
    github.com/go-playground/universal-translator v0.18.1 // indirect
    github.com/go-playground/validator/v10 v10.11.2 // indirect
    github.com/goccy/go-json v0.10.0 // indirect
    github.com/json-iterator/go v1.1.12 // indirect
    github.com/leodido/go-urn v1.2.1 // indirect
    github.com/mattn/go-isatty v0.0.17 // indirect
    github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
    github.com/modern-go/reflect2 v1.0.2 // indirect
    github.com/pelletier/go-toml/v2 v2.0.6 // indirect
    github.com/pmezard/go-difflib v1.0.0 // indirect
    github.com/stretchr/objx v0.5.0 // indirect
    github.com/stretchr/testify v1.8.1 // indirect
    github.com/ugorji/go/codec v1.2.9 // indirect
    golang.org/x/crypto v0.6.0 // indirect
    golang.org/x/net v0.7.0 // indirect
    golang.org/x/sys v0.5.0 // indirect
    golang.org/x/text v0.7.0 // indirect
    google.golang.org/protobuf v1.28.1 // indirect
    gopkg.in/yaml.v2 v2.4.0 // indirect
    gopkg.in/yaml.v3 v3.0.1 // indirect
)

main.go

package main

import (
    "fmt"
    "net/http"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/gin-gonic/gin/binding"
)

type ImportRecord struct {
    Timestamp time.Time `json:"timestamp" example:"2022-11-22T10:00:00Z" validate:"required"`
}

type CreateRecordsRequestBody struct {
    Records []ImportRecord `json:"timeseries"`
}

func main() {
    r := gin.Default()
    r.POST("/", func(c *gin.Context) {
        var model CreateRecordsRequestBody
        if err := c.ShouldBindWith(&model, binding.JSON); err != nil {
            fmt.Printf("request to %s failed while validating request body, error=%v", c.FullPath(), err.Error())
            c.JSON(http.StatusBadRequest, gin.H{
                "detail": err.Error(),
            })
            return
        }
        c.JSON(http.StatusOK, gin.H{
            "message": "pong",
        })
    })

    r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

main_test.go

package main

import (
    "bytes"
    "encoding/json"
    "io/ioutil"
    "net/http"
    "testing"

    "github.com/bradleyjkemp/cupaloy/v2"
    "github.com/stretchr/testify/require"
)

func TestThing(t *testing.T) {
    type record struct {
        Timestamp string `json:"timestamp"`
    }

    type requestBody struct {
        Records []record `json:"timeseries"`
    }

    body := requestBody{
        Records: []record{
            {
                Timestamp: "01.01.2022 22:00",
            },
        },
    }

    buf, err := json.Marshal(body)
    require.Nil(t, err)
    send, err := http.NewRequest(http.MethodPost, "http://localhost:8080", bytes.NewReader(buf))
    require.Nil(t, err)
    res, err := http.DefaultClient.Do(send)
    require.Nil(t, err)
    defer res.Body.Close()

    ss := cupaloy.New(
        cupaloy.SnapshotSubdirectory("testdata/snapshots"), cupaloy.SnapshotFileExtension(".json"),
    )
    resBody, err := ioutil.ReadAll(res.Body)
    require.Nil(t, err)
    ss.SnapshotT(t, resBody)
}

When I run in one terminal go run . and then in another go test ./... with go version 1.19.4 the first time, I get the following error message, as I should as snapshot tests fail the first time you run them because there is no saved file to compare the payload to. But note the escaping around the timestamps:

❯ go test ./...
--- FAIL: TestThing (0.00s)
    main_test.go:50: snapshot created for test TestThing, with contents:
        {"detail":"parsing time \"\\\"01.01.2022 22:00\\\"\" as \"\\\"2006-01-02T15:0
4:05Z07:00\\\"\": cannot parse \"1.2022 22:00\\\"\" as \"2006\""}

I then switched my go version to 1.20 and ran go test ./... again:

❯ go test ./...
--- FAIL: TestThing (0.00s)
    main_test.go:50: snapshot not equal:
        --- Previous
        +++ Current
        @@ -1,2 +1,2 @@
        -{"detail":"parsing time \"\\\"01.01.2022 22:00\\\"\" as \"\\\"2006-01-02T15:
04:05Z07:00\\\"\": cannot parse \"1.2022 22:00\\\"\" as \"2006\""}
        +{"detail":"parsing time \"01.01.2022 22:00\" as \"2006-01-02T15:04:05Z07:00\
": cannot parse \"01.01.2022 22:00\" as \"2006\""}

Note how in the + section the escaping is now better and doesn't include \\\.

Expectations

I would have expected the error messages to stay the same, even though the escaping in 1.19 was worse.

Actual result

Something changed between go versions that caused the escaping of the timestamps to change in Gin's validation errors.

I've also confirmed that this has nothing to do with cupaloy, the snapshot testing library I'm using. I sent requests to my local server from main.go using httpie and have the same escaping discrepancy between go versions:

request.json

{
    "timeseries": [
        {
            "timestamp": "01.01.2022 22:00",
        }
    ]
}

go 1.19.4

❯ http post :8080 < ../request.json
HTTP/1.1 400 Bad Request
Content-Length: 142
Content-Type: application/json; charset=utf-8
Date: Mon, 20 Feb 2023 11:02:13 GMT

{
    "detail": "parsing time \"\\\"01.01.2022 22:00\\\"\" as \"\\\"2006-01-02T15:04:05Z07:00\\\"\": cannot parse \"1.2022 22:00\\\"\" as \"2006\""
}

go 1.20

❯ http post :8080 < ../request.json
HTTP/1.1 400 Bad Request
Content-Length: 126
Content-Type: application/json; charset=utf-8
Date: Mon, 20 Feb 2023 11:03:20 GMT

{
    "detail": "parsing time \"01.01.2022 22:00\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"01.01.2022 22:00\" as \"2006\""
}

Environment

monkey92t commented 1 year ago

In go1.20, enforce strict parsing for RFC3339 for Time.Unmarshal. By default, gin uses func (t *Time) UnmarshalJSON(data []byte) method, See https://github.com/golang/go/issues/54580