フラミナル

考え方や調べたことを書き殴ります。IT技術系記事多め

echo におけるjsonで受け取ったデータが一部勝手にbase64 decodeされる

echo を使って /test で json を受け入れるサーバを立ち上げます。

package main

import (
    "context"
    "fmt"
    "net/http"

    "github.com/labstack/echo/v4"
)

type Message struct {
    Name []byte `json:"name"`
}

func main() {
    e := echo.New()

    e.POST("/test", func(ctx echo.Context) error {
        var m Message

        if err := ctx.Bind(&m); err != nil {
            return err
        }
        fmt.Println(string(m.Name))
        return nil
    })

    go e.Start(":8080")

    ctx := context.Background()
    select {
    case <-ctx.Done():
        break
    }

    return
}

このサーバに対して以下のリクエストを送ると、

curl localhost:8080/test -X POST -d '{"name": "ZnVnYQ=="}' -H "Content-Type: application/json"

サーバは fuga を標準出力します。元々は base64 encode された文字列 ZnVnYQ== だったのにどこかで decode されているようです。

結論

ここの Bind のなかでやっています。

if err := ctx.Bind(&m); err != nil {
    return err
}

詳しく追っていくとこの Bind では BindBody を呼び出します。BindBody では ContentType Header をみて json である時に JSONSerializer.Deserialize を実行します。

func (b *DefaultBinder) BindBody(c Context, i interface{}) (err error) {
    req := c.Request()
    if req.ContentLength == 0 {
        return
    }

    ctype := req.Header.Get(HeaderContentType)
    switch {
    case strings.HasPrefix(ctype, MIMEApplicationJSON):
        if err = c.Echo().JSONSerializer.Deserialize(c, i); err != nil {
            switch err.(type) {
            case *HTTPError:
                return err
            default:
                return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
            }
        }

JSONSerializer.Deserialize は裏側で json.unmarshal を呼び出しでデシリアライズをするのですが、その先では以下の処理を行なっています。

ここでは Bind 先の変数の型をチェックしており、reflect.Slice かつ reflect.Uint8 の場合は base64.StdEncoding.Decode(b, s) をしています。

       switch v.Kind() {
        default:
            d.saveError(&UnmarshalTypeError{Value: "string", Type: v.Type(), Offset: int64(d.readIndex())})
        case reflect.Slice:
            if v.Type().Elem().Kind() != reflect.Uint8 {
                d.saveError(&UnmarshalTypeError{Value: "string", Type: v.Type(), Offset: int64(d.readIndex())})
                break
            }
            b := make([]byte, base64.StdEncoding.DecodedLen(len(s)))
            n, err := base64.StdEncoding.Decode(b, s)
            if err != nil {
                d.saveError(err)
                break
            }
            v.SetBytes(b[:n])

今回の echo サーバーでは Message 構造体の Name は []byte であり、

type Message struct {
    Name []byte `json:"name"`
}

Go において byteuint8 の type alias であるため、Name はこの if 文の中に入ることができ、結果として ZnVnYQ== は base64 decode されたのちに Message.Name に格納されることになります。

- The Go Programming Language

// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
// used, by convention, to distinguish byte values from 8-bit unsigned
// integer values.
type byte = uint8

ついでに試してみる

base64 encode して curl

curl localhost:8080/test -X POST -d '{"name": "ZnVnYQ=="}' -H "Content-Type: application/json"

普通に表示される。

fuga

base64 decode せずに curl

curl localhost:8080/test -X POST -d '{"name": "test"}' -H "Content-Type: application/json"

このような表示になります。(test を無理やり base64 decode しようとする)

��-

application/json をつけずに curl

curl localhost:8080/test -X POST -d '{"name": "ZnVnYQ=="}'

何も表示されない。(body を json 解釈しないからかな)

ついでに

このような仕様が json や http に規定されているのか?を ChatGPT さんに聞いてみたけど、そんなことはないみたい。(Go の encoding/json に閉じた話とのこと)