| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184 |
- package httputil
- import (
- "bytes"
- "errors"
- "net/http"
- "net/http/httptest"
- "testing"
- "github.com/tidwall/gjson"
- )
- func TestNormalizeLenientJSONRequestBody_accepts_client_control_chars_in_strings(t *testing.T) {
- tests := []struct {
- name string
- body []byte
- path string
- want string
- wantRaw string
- }{
- {
- name: "null byte in message content",
- body: []byte("{\"messages\":[{\"content\":\"hello\x00world\"}]}"),
- path: "messages.0.content",
- want: "hello\x00world",
- wantRaw: `"hello\u0000world"`,
- },
- {
- name: "ansi escape in message content",
- body: []byte("{\"messages\":[{\"content\":\"hello\x1b[31mred\x1b[0m\"}]}"),
- path: "messages.0.content",
- want: "hello\x1b[31mred\x1b[0m",
- wantRaw: `"hello\u001b[31mred\u001b[0m"`,
- },
- {
- name: "leading UTF-8 BOM",
- body: []byte("\xef\xbb\xbf{\"input\":\"hello\"}"),
- path: "input",
- want: "hello",
- wantRaw: `"hello"`,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- // Given
- if gjson.ValidBytes(tt.body) {
- t.Fatalf("test payload should reproduce strict JSON rejection: %q", tt.body)
- }
- // When
- got, err := NormalizeLenientJSONRequestBody(tt.body, 1024)
- if err != nil {
- t.Fatalf("NormalizeLenientJSONRequestBody: %v", err)
- }
- // Then
- if !gjson.ValidBytes(got) {
- t.Fatalf("normalized body should be valid JSON: %q", got)
- }
- result := gjson.GetBytes(got, tt.path)
- if result.String() != tt.want {
- t.Fatalf("value mismatch: got %q want %q", result.String(), tt.want)
- }
- if result.Raw != tt.wantRaw {
- t.Fatalf("raw value mismatch: got %q want %q", result.Raw, tt.wantRaw)
- }
- })
- }
- }
- func TestNormalizeLenientJSONRequestBody_keeps_invalid_structure_invalid(t *testing.T) {
- tests := []struct {
- name string
- body []byte
- }{
- {
- name: "truncated JSON",
- body: []byte("{\"messages\":[{\"content\":\"hello\"}]"),
- },
- {
- name: "control character outside string",
- body: []byte("{\"input\":\"hello\"}\x00"),
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- // When
- got, err := NormalizeLenientJSONRequestBody(tt.body, 1024)
- if err != nil {
- t.Fatalf("NormalizeLenientJSONRequestBody: %v", err)
- }
- // Then
- if gjson.ValidBytes(got) {
- t.Fatalf("normalization must not repair invalid JSON structure: %q", got)
- }
- })
- }
- }
- func TestNormalizeLenientJSONRequestBody_allows_http_requests_with_client_control_chars(t *testing.T) {
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- // Given
- body, err := ReadLenientJSONRequestBodyWithPrealloc(r, 1024)
- if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
- // When
- if !gjson.ValidBytes(body) {
- http.Error(w, "Failed to parse request body", http.StatusBadRequest)
- return
- }
- w.WriteHeader(http.StatusAccepted)
- }))
- defer server.Close()
- tests := []struct {
- name string
- body []byte
- want int
- }{
- {
- name: "null byte in JSON string",
- body: []byte("{\"model\":\"gpt-5.5\",\"messages\":[{\"role\":\"user\",\"content\":\"hello\x00world\"}]}"),
- want: http.StatusAccepted,
- },
- {
- name: "ANSI escape in JSON string",
- body: []byte("{\"model\":\"gpt-5.5\",\"messages\":[{\"role\":\"user\",\"content\":\"hello\x1b[31mred\x1b[0m\"}]}"),
- want: http.StatusAccepted,
- },
- {
- name: "leading UTF-8 BOM",
- body: []byte("\xef\xbb\xbf{\"model\":\"gpt-5.5\",\"messages\":[{\"role\":\"user\",\"content\":\"hello\"}]}"),
- want: http.StatusAccepted,
- },
- {
- name: "truncated JSON",
- body: []byte("{\"model\":\"gpt-5.5\",\"messages\":[{\"role\":\"user\",\"content\":\"hello\"}]"),
- want: http.StatusBadRequest,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- req, err := http.NewRequest(http.MethodPost, server.URL+"/v1/chat/completions", bytes.NewReader(tt.body))
- if err != nil {
- t.Fatalf("NewRequest: %v", err)
- }
- req.Header.Set("Content-Type", "application/json")
- resp, err := server.Client().Do(req)
- if err != nil {
- t.Fatalf("Do: %v", err)
- }
- defer func() { _ = resp.Body.Close() }()
- if resp.StatusCode != tt.want {
- t.Fatalf("status mismatch: got %d want %d", resp.StatusCode, tt.want)
- }
- })
- }
- }
- func TestNormalizeLenientJSONRequestBody_rejects_expansion_past_limit(t *testing.T) {
- // Given
- body := []byte("{\"input\":\"\x00\x00\"}")
- // When
- _, err := NormalizeLenientJSONRequestBody(body, int64(len(body)+5))
- // Then
- var maxErr *http.MaxBytesError
- if !errors.As(err, &maxErr) {
- t.Fatalf("expected MaxBytesError, got %T %v", err, err)
- }
- if maxErr.Limit != int64(len(body)+5) {
- t.Fatalf("limit mismatch: got %d want %d", maxErr.Limit, len(body)+5)
- }
- }
|