mirror of
https://github.com/zeromicro/go-zero.git
synced 2026-05-14 02:10:00 +08:00
fix(httpx): Resolve HTML escaping issue during JSON serialization (#5032)
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package httpx
|
package httpx
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -172,8 +173,28 @@ func doHandleError(w http.ResponseWriter, err error, handler func(error) (int, a
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func doMarshalJson(v any) ([]byte, error) {
|
||||||
|
// why not use json.Marshal? https://github.com/golang/go/issues/28453
|
||||||
|
// it change the behavior of json.Marshal, like & -> \u0026, < -> \u003c, > -> \u003e
|
||||||
|
// which is not what we want in logic response
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
enc := json.NewEncoder(buf)
|
||||||
|
enc.SetEscapeHTML(false)
|
||||||
|
if err := enc.Encode(v); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bs := buf.Bytes()
|
||||||
|
// Remove trailing newline added by json.Encoder.Encode
|
||||||
|
if len(bs) > 0 && bs[len(bs)-1] == '\n' {
|
||||||
|
bs = bs[:len(bs)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return bs, nil
|
||||||
|
}
|
||||||
|
|
||||||
func doWriteJson(w http.ResponseWriter, code int, v any) error {
|
func doWriteJson(w http.ResponseWriter, code int, v any) error {
|
||||||
bs, err := json.Marshal(v)
|
bs, err := doMarshalJson(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return fmt.Errorf("marshal json failed, error: %w", err)
|
return fmt.Errorf("marshal json failed, error: %w", err)
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/zeromicro/go-zero/core/logx"
|
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
)
|
)
|
||||||
|
|
||||||
type message struct {
|
type message struct {
|
||||||
@@ -443,3 +444,103 @@ func TestWriteJsonCtxMarshalFailed(t *testing.T) {
|
|||||||
})
|
})
|
||||||
assert.Equal(t, http.StatusInternalServerError, w.code)
|
assert.Equal(t, http.StatusInternalServerError, w.code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_doMarshalJson(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
v any
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want []byte
|
||||||
|
wantErr assert.ErrorAssertionFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil",
|
||||||
|
args: args{nil},
|
||||||
|
want: []byte("null"),
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "string",
|
||||||
|
args: args{"hello"},
|
||||||
|
want: []byte(`"hello"`),
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "int",
|
||||||
|
args: args{42},
|
||||||
|
want: []byte("42"),
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bool",
|
||||||
|
args: args{true},
|
||||||
|
want: []byte("true"),
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "struct",
|
||||||
|
args: args{
|
||||||
|
struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}{Name: "test"},
|
||||||
|
},
|
||||||
|
want: []byte(`{"name":"test"}`),
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "slice",
|
||||||
|
args: args{[]int{1, 2, 3}},
|
||||||
|
want: []byte("[1,2,3]"),
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "map",
|
||||||
|
args: args{map[string]int{"a": 1, "b": 2}},
|
||||||
|
want: []byte(`{"a":1,"b":2}`),
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unmarshalable type",
|
||||||
|
args: args{complex(1, 2)},
|
||||||
|
want: nil,
|
||||||
|
wantErr: assert.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "channel type",
|
||||||
|
args: args{make(chan int)},
|
||||||
|
want: nil,
|
||||||
|
wantErr: assert.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "url with query params",
|
||||||
|
args: args{"https://example.com/api?name=test&age=25"},
|
||||||
|
want: []byte(`"https://example.com/api?name=test&age=25"`),
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "url with encoded query params",
|
||||||
|
args: args{"https://example.com/api?data=hello%20world&special=%26%3D"},
|
||||||
|
want: []byte(`"https://example.com/api?data=hello%20world&special=%26%3D"`),
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "url with multiple query params",
|
||||||
|
args: args{"http://localhost:8080/users?page=1&limit=10&sort=name&order=asc"},
|
||||||
|
want: []byte(`"http://localhost:8080/users?page=1&limit=10&sort=name&order=asc"`),
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(
|
||||||
|
tt.name, func(t *testing.T) {
|
||||||
|
got, err := doMarshalJson(tt.args.v)
|
||||||
|
if !tt.wantErr(t, err, fmt.Sprintf("doMarshalJson(%v)", tt.args.v)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equalf(t, string(tt.want), string(got), "doMarshalJson(%v)", tt.args.v)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user