feat: support quoted message headers

This commit is contained in:
richjohnrambo
2026-09-23 17:46:51 +08:00
parent aa9f562e1e
commit 3e92021254
5 changed files with 235 additions and 1 deletions
+75
View File
@@ -3727,6 +3727,13 @@ func validateMessageHeaders(head types.KVMap) error {
return types.ErrMalformed
}
}
if rawReply, ok := head["x-im-reply"]; ok {
reply, err := normalizeIMReplyHeader(rawReply)
if err != nil {
return err
}
head["x-im-reply"] = reply
}
return nil
}
@@ -3779,6 +3786,74 @@ func messageHeaderString(head types.KVMap, key string) (string, bool) {
return text, text != ""
}
func normalizeIMReplyHeader(value any) (map[string]any, error) {
var reply map[string]any
switch typed := value.(type) {
case map[string]any:
reply = typed
case types.KVMap:
reply = map[string]any(typed)
case string:
if strings.TrimSpace(typed) == "" {
return nil, types.ErrMalformed
}
if err := json.Unmarshal([]byte(typed), &reply); err != nil {
return nil, types.ErrMalformed
}
default:
return nil, types.ErrMalformed
}
if reply == nil || !validIMReplySeq(reply["seq"]) {
return nil, types.ErrMalformed
}
if text, ok := reply["type"].(string); !ok || strings.TrimSpace(text) == "" {
return nil, types.ErrMalformed
}
if text, ok := reply["text"].(string); !ok || strings.TrimSpace(text) == "" {
return nil, types.ErrMalformed
}
if from, ok := reply["from"]; ok {
if text, ok := from.(string); !ok || strings.TrimSpace(text) == "" {
return nil, types.ErrMalformed
}
}
return reply, nil
}
func validIMReplySeq(value any) bool {
switch typed := value.(type) {
case json.Number:
_, err := typed.Int64()
return err == nil
case float64:
return typed > 0 && typed == float64(int64(typed))
case float32:
return typed > 0 && typed == float32(int64(typed))
case int:
return typed > 0
case int8:
return typed > 0
case int16:
return typed > 0
case int32:
return typed > 0
case int64:
return typed > 0
case uint:
return typed > 0
case uint8:
return typed > 0
case uint16:
return typed > 0
case uint32:
return typed > 0
case uint64:
return typed > 0
default:
return false
}
}
func validIMMessageHeaderToken(value string, max int) bool {
value = strings.TrimSpace(value)
if value == "" || len(value) > max {
+24
View File
@@ -23,6 +23,8 @@ func TestValidateMessageHeadersProductContract(t *testing.T) {
{"x-im-schema": 1, "type": "audio", "x-im-type": "voice"},
{"x-im-schema": 1, "x-im-type": "system_notice"},
{"x-im-schema": 1, "x-im-type": "system_action"},
{"x-im-schema": 1, "x-im-type": "text", "x-im-reply": map[string]any{"seq": float64(128), "type": "text", "text": "hello", "from": "usr1"}},
{"x-im-schema": 1, "x-im-type": "text", "x-im-reply": `{"seq":128,"type":"text","text":"hello","from":"usr1"}`},
{"type": "system"},
}
for _, head := range valid {
@@ -36,6 +38,10 @@ func TestValidateMessageHeadersProductContract(t *testing.T) {
{"x-im-schema": true, "x-im-type": "text"},
{"x-im-schema": float64(1), "x-im-type": "unknown"},
{"type": "text", "x-im-type": "image"},
{"x-im-schema": 1, "x-im-type": "text", "x-im-reply": map[string]any{"seq": 0, "type": "text", "text": "hello"}},
{"x-im-schema": 1, "x-im-type": "text", "x-im-reply": map[string]any{"seq": 1.5, "type": "text", "text": "hello"}},
{"x-im-schema": 1, "x-im-type": "text", "x-im-reply": map[string]any{"seq": 1, "type": "text"}},
{"x-im-schema": 1, "x-im-type": "text", "x-im-reply": "not-json"},
}
for _, head := range invalid {
if err := validateMessageHeaders(head); err != types.ErrMalformed {
@@ -145,3 +151,21 @@ func TestActionTokenCachePreservesInternalFields(t *testing.T) {
t.Fatalf("cache round trip lost fields: got %#v", got)
}
}
func TestValidateMessageHeadersNormalizesReplyJSONString(t *testing.T) {
head := types.KVMap{
"x-im-schema": 1,
"x-im-type": "text",
"x-im-reply": `{"seq":128,"type":"text","text":"hello","from":"usr1"}`,
}
if err := validateMessageHeaders(head); err != nil {
t.Fatalf("validateMessageHeaders() = %v, want nil", err)
}
reply, ok := head["x-im-reply"].(map[string]any)
if !ok {
t.Fatalf("x-im-reply was not normalized to object: %#v", head["x-im-reply"])
}
if reply["seq"].(float64) != 128 || reply["type"] != "text" || reply["text"] != "hello" || reply["from"] != "usr1" {
t.Fatalf("unexpected normalized reply: %#v", reply)
}
}