fix(messaging): normalize numeric webviewId values in JSON payloads
The message handler now gracefully handles both string and numeric webviewId formats from the frontend. When standard JSON unmarshaling fails, the parser attempts to normalize numeric IDs to strings before re-parsing. This prevents unmarshaling errors when frontend code sends webviewId as a number instead of the expected string format. Changes: - Add parseIncomingMessage() with fallback normalization logic - Add normalizeWebViewIDPayload() to convert numeric IDs to strings - Add parseWebViewIDRaw() to handle both string and number formats - Add comprehensive test coverage for both string and numeric cases Fixes frontend-backend compatibility issues with webviewId field types.
- commit
- 61536e8cd1ef8f17f750e9ee4c8eb42a7cf27145
- author
- bnema · · 2025-11-17 21:03 UTC
- committer
- bnema · · 2025-11-17 19:07 UTC
- parent
- de91544
- signature
- unsigned
3 files · +105 -2
M CHANGELOG.md +12 -0
| @@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file. | |||
| 4 | 4 | ||
| 5 | 5 | ## [Unreleased] | |
| 6 | 6 | ||
| 7 | + | ### Added | |
| 8 | + | - **Cloudflare Turnstile CAPTCHA support**: Temporary CORS allowlist workaround for `challenges.cloudflare.com` to bypass WebKit's incomplete COEP credentialless implementation. Allows Turnstile-protected sites to function properly. Configurable via `enable_turnstile_workaround` (default: true). | |
| 9 | + | - **Global content filtering toggle**: New `content_filtering.enabled` config option to completely disable ad blocking when needed. Whitelist system remains available for granular control. Defaults to enabled for backward compatibility. | |
| 10 | + | ||
| 11 | + | ### Fixed | |
| 12 | + | - **OAuth popup handling**: Fixed OAuth popups not functioning in stacked panes and split operations by ensuring `setupPopupHandling()` is called in all pane creation code paths (finalizeStackCreation, CloseStackedPane, splitNode). | |
| 13 | + | - **WebView ID message parsing**: Fixed JSON unmarshaling errors when frontend sends numeric webviewId values instead of strings. Message handler now gracefully normalizes both string and numeric webviewId formats with fallback parsing logic. | |
| 14 | + | ||
| 15 | + | ### Changed | |
| 16 | + | - **Console output cleanup**: Disabled WebKit console.log() messages to stdout to prevent page script spam. DevTools console (F12) still shows all messages. | |
| 17 | + | - **Content filtering configuration**: Refactored `content_filtering_whitelist` into structured `content_filtering` config with `enabled` boolean and `whitelist` array for better organization. | |
| 18 | + | ||
| 7 | 19 | ## [0.14.0] - 2025-11-18 | |
| 8 | 20 | ||
| 9 | 21 | ### Added | |
| @@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file. | |||||
| 4 | 4 | ||||
| 5 | ## [Unreleased] | 5 | ## [Unreleased] | ||
| 6 | 6 | ||||
| 7 | + | ### Added | |||
| 8 | + | - **Cloudflare Turnstile CAPTCHA support**: Temporary CORS allowlist workaround for `challenges.cloudflare.com` to bypass WebKit's incomplete COEP credentialless implementation. Allows Turnstile-protected sites to function properly. Configurable via `enable_turnstile_workaround` (default: true). | |||
| 9 | + | - **Global content filtering toggle**: New `content_filtering.enabled` config option to completely disable ad blocking when needed. Whitelist system remains available for granular control. Defaults to enabled for backward compatibility. | |||
| 10 | + | ||||
| 11 | + | ### Fixed | |||
| 12 | + | - **OAuth popup handling**: Fixed OAuth popups not functioning in stacked panes and split operations by ensuring `setupPopupHandling()` is called in all pane creation code paths (finalizeStackCreation, CloseStackedPane, splitNode). | |||
| 13 | + | - **WebView ID message parsing**: Fixed JSON unmarshaling errors when frontend sends numeric webviewId values instead of strings. Message handler now gracefully normalizes both string and numeric webviewId formats with fallback parsing logic. | |||
| 14 | + | ||||
| 15 | + | ### Changed | |||
| 16 | + | - **Console output cleanup**: Disabled WebKit console.log() messages to stdout to prevent page script spam. DevTools console (F12) still shows all messages. | |||
| 17 | + | - **Content filtering configuration**: Refactored `content_filtering_whitelist` into structured `content_filtering` config with `enabled` boolean and `whitelist` array for better organization. | |||
| 18 | + | ||||
| 7 | ## [0.14.0] - 2025-11-18 | 19 | ## [0.14.0] - 2025-11-18 | ||
| 8 | 20 | ||||
| 9 | ### Added | 21 | ### Added | ||
M internal/app/messaging/handler.go +66 -2
| @@ -1,8 +1,10 @@ | |||
| 1 | 1 | package messaging | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "bytes" | |
| 4 | 5 | "context" | |
| 5 | 6 | "encoding/json" | |
| 7 | + | "fmt" | |
| 6 | 8 | "log" | |
| 7 | 9 | "strconv" | |
| 8 | 10 | "time" | |
| @@ -75,8 +77,8 @@ func (h *Handler) SetWorkspaceObserver(observer WorkspaceObserver) { | |||
| 75 | 77 | ||
| 76 | 78 | // Handle processes incoming script messages | |
| 77 | 79 | func (h *Handler) Handle(payload string) { | |
| 78 | - | var msg Message | |
| 79 | - | if err := json.Unmarshal([]byte(payload), &msg); err != nil { | |
| 80 | + | msg, err := parseIncomingMessage(payload) | |
| 81 | + | if err != nil { | |
| 80 | 82 | log.Printf("[ERROR] Failed to unmarshal message: %v", err) | |
| 81 | 83 | return | |
| 82 | 84 | } | |
| @@ -122,6 +124,68 @@ func (h *Handler) Handle(payload string) { | |||
| 122 | 124 | } | |
| 123 | 125 | } | |
| 124 | 126 | ||
| 127 | + | func parseIncomingMessage(payload string) (Message, error) { | |
| 128 | + | data := []byte(payload) | |
| 129 | + | var msg Message | |
| 130 | + | if err := json.Unmarshal(data, &msg); err == nil { | |
| 131 | + | return msg, nil | |
| 132 | + | } else { | |
| 133 | + | normalized, normErr := normalizeWebViewIDPayload(data) | |
| 134 | + | if normErr != nil { | |
| 135 | + | return Message{}, err | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | if err := json.Unmarshal(normalized, &msg); err != nil { | |
| 139 | + | return Message{}, err | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | return msg, nil | |
| 143 | + | } | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | func normalizeWebViewIDPayload(data []byte) ([]byte, error) { | |
| 147 | + | var raw map[string]json.RawMessage | |
| 148 | + | if err := json.Unmarshal(data, &raw); err != nil { | |
| 149 | + | return nil, err | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | rawID, ok := raw["webviewId"] | |
| 153 | + | if !ok { | |
| 154 | + | return nil, fmt.Errorf("webviewId missing in payload") | |
| 155 | + | } | |
| 156 | + | ||
| 157 | + | normalizedID, err := parseWebViewIDRaw(rawID) | |
| 158 | + | if err != nil { | |
| 159 | + | return nil, err | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | raw["webviewId"] = json.RawMessage(strconv.Quote(normalizedID)) | |
| 163 | + | ||
| 164 | + | return json.Marshal(raw) | |
| 165 | + | } | |
| 166 | + | ||
| 167 | + | func parseWebViewIDRaw(raw json.RawMessage) (string, error) { | |
| 168 | + | trimmed := bytes.TrimSpace(raw) | |
| 169 | + | if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) { | |
| 170 | + | return "", nil | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | if trimmed[0] == '"' { | |
| 174 | + | var id string | |
| 175 | + | if err := json.Unmarshal(trimmed, &id); err != nil { | |
| 176 | + | return "", err | |
| 177 | + | } | |
| 178 | + | return id, nil | |
| 179 | + | } | |
| 180 | + | ||
| 181 | + | var number json.Number | |
| 182 | + | if err := json.Unmarshal(trimmed, &number); err == nil { | |
| 183 | + | return number.String(), nil | |
| 184 | + | } | |
| 185 | + | ||
| 186 | + | return "", fmt.Errorf("unsupported webviewId format: %s", string(trimmed)) | |
| 187 | + | } | |
| 188 | + | ||
| 125 | 189 | // SetWebView sets the WebView reference (needed for script injection) | |
| 126 | 190 | func (h *Handler) SetWebView(webView *webkit.WebView) { | |
| 127 | 191 | h.webView = webView | |
| @@ -1,8 +1,10 @@ | |||||
| 1 | package messaging | 1 | package messaging | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "bytes" | |||
| 4 | "context" | 5 | "context" | ||
| 5 | "encoding/json" | 6 | "encoding/json" | ||
| 7 | + | "fmt" | |||
| 6 | "log" | 8 | "log" | ||
| 7 | "strconv" | 9 | "strconv" | ||
| 8 | "time" | 10 | "time" | ||
| @@ -75,8 +77,8 @@ func (h *Handler) SetWorkspaceObserver(observer WorkspaceObserver) { | |||||
| 75 | 77 | ||||
| 76 | // Handle processes incoming script messages | 78 | // Handle processes incoming script messages | ||
| 77 | func (h *Handler) Handle(payload string) { | 79 | func (h *Handler) Handle(payload string) { | ||
| 78 | - | var msg Message | 80 | + | msg, err := parseIncomingMessage(payload) |
| 79 | - | if err := json.Unmarshal([]byte(payload), &msg); err != nil { | 81 | + | if err != nil { |
| 80 | log.Printf("[ERROR] Failed to unmarshal message: %v", err) | 82 | log.Printf("[ERROR] Failed to unmarshal message: %v", err) | ||
| 81 | return | 83 | return | ||
| 82 | } | 84 | } | ||
| @@ -122,6 +124,68 @@ func (h *Handler) Handle(payload string) { | |||||
| 122 | } | 124 | } | ||
| 123 | } | 125 | } | ||
| 124 | 126 | ||||
| 127 | + | func parseIncomingMessage(payload string) (Message, error) { | |||
| 128 | + | data := []byte(payload) | |||
| 129 | + | var msg Message | |||
| 130 | + | if err := json.Unmarshal(data, &msg); err == nil { | |||
| 131 | + | return msg, nil | |||
| 132 | + | } else { | |||
| 133 | + | normalized, normErr := normalizeWebViewIDPayload(data) | |||
| 134 | + | if normErr != nil { | |||
| 135 | + | return Message{}, err | |||
| 136 | + | } | |||
| 137 | + | ||||
| 138 | + | if err := json.Unmarshal(normalized, &msg); err != nil { | |||
| 139 | + | return Message{}, err | |||
| 140 | + | } | |||
| 141 | + | ||||
| 142 | + | return msg, nil | |||
| 143 | + | } | |||
| 144 | + | } | |||
| 145 | + | ||||
| 146 | + | func normalizeWebViewIDPayload(data []byte) ([]byte, error) { | |||
| 147 | + | var raw map[string]json.RawMessage | |||
| 148 | + | if err := json.Unmarshal(data, &raw); err != nil { | |||
| 149 | + | return nil, err | |||
| 150 | + | } | |||
| 151 | + | ||||
| 152 | + | rawID, ok := raw["webviewId"] | |||
| 153 | + | if !ok { | |||
| 154 | + | return nil, fmt.Errorf("webviewId missing in payload") | |||
| 155 | + | } | |||
| 156 | + | ||||
| 157 | + | normalizedID, err := parseWebViewIDRaw(rawID) | |||
| 158 | + | if err != nil { | |||
| 159 | + | return nil, err | |||
| 160 | + | } | |||
| 161 | + | ||||
| 162 | + | raw["webviewId"] = json.RawMessage(strconv.Quote(normalizedID)) | |||
| 163 | + | ||||
| 164 | + | return json.Marshal(raw) | |||
| 165 | + | } | |||
| 166 | + | ||||
| 167 | + | func parseWebViewIDRaw(raw json.RawMessage) (string, error) { | |||
| 168 | + | trimmed := bytes.TrimSpace(raw) | |||
| 169 | + | if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) { | |||
| 170 | + | return "", nil | |||
| 171 | + | } | |||
| 172 | + | ||||
| 173 | + | if trimmed[0] == '"' { | |||
| 174 | + | var id string | |||
| 175 | + | if err := json.Unmarshal(trimmed, &id); err != nil { | |||
| 176 | + | return "", err | |||
| 177 | + | } | |||
| 178 | + | return id, nil | |||
| 179 | + | } | |||
| 180 | + | ||||
| 181 | + | var number json.Number | |||
| 182 | + | if err := json.Unmarshal(trimmed, &number); err == nil { | |||
| 183 | + | return number.String(), nil | |||
| 184 | + | } | |||
| 185 | + | ||||
| 186 | + | return "", fmt.Errorf("unsupported webviewId format: %s", string(trimmed)) | |||
| 187 | + | } | |||
| 188 | + | ||||
| 125 | // SetWebView sets the WebView reference (needed for script injection) | 189 | // SetWebView sets the WebView reference (needed for script injection) | ||
| 126 | func (h *Handler) SetWebView(webView *webkit.WebView) { | 190 | func (h *Handler) SetWebView(webView *webkit.WebView) { | ||
| 127 | h.webView = webView | 191 | h.webView = webView | ||
A internal/app/messaging/handler_test.go +27 -0
| @@ -0,0 +1,27 @@ | |||
| 1 | + | package messaging | |
| 2 | + | ||
| 3 | + | import "testing" | |
| 4 | + | ||
| 5 | + | func TestParseIncomingMessageAcceptsNumericWebViewID(t *testing.T) { | |
| 6 | + | payload := `{"type":"workspace","event":"focus","webviewId":42}` | |
| 7 | + | msg, err := parseIncomingMessage(payload) | |
| 8 | + | if err != nil { | |
| 9 | + | t.Fatalf("unexpected error parsing payload: %v", err) | |
| 10 | + | } | |
| 11 | + | ||
| 12 | + | if msg.WebViewID != "42" { | |
| 13 | + | t.Fatalf("expected webviewId to be \"42\", got %q", msg.WebViewID) | |
| 14 | + | } | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | func TestParseIncomingMessagePreservesStringWebViewID(t *testing.T) { | |
| 18 | + | payload := `{"type":"workspace","event":"focus","webviewId":"popup-123"}` | |
| 19 | + | msg, err := parseIncomingMessage(payload) | |
| 20 | + | if err != nil { | |
| 21 | + | t.Fatalf("unexpected error parsing payload: %v", err) | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | if msg.WebViewID != "popup-123" { | |
| 25 | + | t.Fatalf("expected webviewId to remain popup-123, got %q", msg.WebViewID) | |
| 26 | + | } | |
| 27 | + | } | |
| @@ -0,0 +1,27 @@ | |||||
| 1 | + | package messaging | |||
| 2 | + | ||||
| 3 | + | import "testing" | |||
| 4 | + | ||||
| 5 | + | func TestParseIncomingMessageAcceptsNumericWebViewID(t *testing.T) { | |||
| 6 | + | payload := `{"type":"workspace","event":"focus","webviewId":42}` | |||
| 7 | + | msg, err := parseIncomingMessage(payload) | |||
| 8 | + | if err != nil { | |||
| 9 | + | t.Fatalf("unexpected error parsing payload: %v", err) | |||
| 10 | + | } | |||
| 11 | + | ||||
| 12 | + | if msg.WebViewID != "42" { | |||
| 13 | + | t.Fatalf("expected webviewId to be \"42\", got %q", msg.WebViewID) | |||
| 14 | + | } | |||
| 15 | + | } | |||
| 16 | + | ||||
| 17 | + | func TestParseIncomingMessagePreservesStringWebViewID(t *testing.T) { | |||
| 18 | + | payload := `{"type":"workspace","event":"focus","webviewId":"popup-123"}` | |||
| 19 | + | msg, err := parseIncomingMessage(payload) | |||
| 20 | + | if err != nil { | |||
| 21 | + | t.Fatalf("unexpected error parsing payload: %v", err) | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | if msg.WebViewID != "popup-123" { | |||
| 25 | + | t.Fatalf("expected webviewId to remain popup-123, got %q", msg.WebViewID) | |||
| 26 | + | } | |||
| 27 | + | } | |||