Deep Review: 20260330-155444-pr-198

Date2026-03-30 15:54
Reporancher-sandbox/rancher-desktop-daemon
Round2
Author@mook-as
PR#198 — mock controller: Add a mock logs endpoint
Branchmook-as/container-api/mock-logs
Commits1b7060e mock controller: Add a mock logs endpoint
ReviewersClaude Opus 4.6, Codex GPT 5.4, Gemini 3.1 Pro
VerdictMerge with fixes — WebSocket upgrader rejects browser clients; error responses mask backend failures
Wall-clock time13 min 29 s

Executive Summary

This PR adds a mock WebSocket logs endpoint (/passthrough/mock/logs/{container}) that streams container labels as log lines, intended for UI development. It wires the endpoint into the mock controller's passthrough discovery, extracts a reusable curl_has_websocket_support helper, and adds BATS tests covering valid containers, invalid IDs, and nonexistent containers. The main concern: the WebSocket upgrader's default origin check rejects browser connections — the primary use case this endpoint serves.


Critical Issues

None.


Important Issues

1. Browser clients rejected by default WebSocket origin check (important, regression) Claude Codex Gemini

The zero-value Upgrader uses gorilla/websocket's default checkSameOrigin, which rejects requests whose Origin header does not match Host. Browser WebSocket requests always send Origin. When the UI runs on a separate dev server (e.g., Vite on port 5173) or in Electron with a custom origin, the origins diverge and the upgrade fails with 403. The BATS tests pass because curl omits the Origin header, so checkSameOrigin returns true unconditionally.

	log.V(5).Info("Failed to get container", "error", err)
		http.Error(w, "Failed to get container", http.StatusNotFound)
		return
	}

	upgrader := websocket.Upgrader{}
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.V(5).Info("Failed to upgrade to WebSocket", "error", err)
		return
	}

The existing demo controller at pkg/controllers/app/demo/controller.go:90-92 explicitly bypasses this check.

func (c *controller) handleWebSocket(w http.ResponseWriter, r *http.Request) {
	log := ctrl.LoggerFrom(r.Context())
	upgrader := &websocket.Upgrader{
		CheckOrigin: func(_ *http.Request) bool { return true },
	}
	conn, err := upgrader.Upgrade(w, r, nil)

Confirmed via git blame: introduced in commit 1b7060e.

Fix: Match the demo controller's pattern.

-	upgrader := websocket.Upgrader{}
+	upgrader := websocket.Upgrader{
+		CheckOrigin: func(_ *http.Request) bool { return true },
+	}

Suggestions

1. All API errors return 404 (suggestion, regression) Claude Gemini

Any error from GetClient().Get() — not-found, kine database issue, context cancellation — returns HTTP 404. During UI development, this masks real backend problems as "container not found." Distinguishing not-found from internal errors improves debuggability.

	var container v1alpha1.Container
	err := c.mgr.GetClient().Get(r.Context(), types.NamespacedName{
		Namespace: apiNamespace,
		Name:      containerID,
	}, &container)
	if err != nil {
		log.V(5).Info("Failed to get container", "error", err)
		http.Error(w, "Failed to get container", http.StatusNotFound)
		return
	}

Fix: Use apierrors.IsNotFound to differentiate (already imported in mock_controller.go).

+	if apierrors.IsNotFound(err) {
+		log.V(5).Info("Container not found", "container", containerID)
+		http.Error(w, "Container not found", http.StatusNotFound)
+		return
+	}
 	if err != nil {
-		log.V(5).Info("Failed to get container", "error", err)
-		http.Error(w, "Failed to get container", http.StatusNotFound)
+		log.V(5).Info("Failed to get container", "container", containerID, "error", err)
+		http.Error(w, "Failed to get container", http.StatusInternalServerError)
 		return
 	}
2. Trailing path segments silently ignored (suggestion, gap) Gemini

strings.Cut extracts the first path segment as containerID and discards the rest. A request to /passthrough/mock/logs/1234/extra/stuff succeeds with container ID 1234. While harmless for a mock endpoint, it can mask malformed frontend requests during development.

func (c *controller) handleLogs(w http.ResponseWriter, r *http.Request) {
	log := ctrl.LoggerFrom(r.Context())
	containerID, _, _ := strings.Cut(strings.TrimLeft(r.URL.Path, "/"), "/")
	log.Info("Handling logs request", "container", containerID)

	if !containerIDValidator.MatchString(containerID) {
		log.V(5).Info("Invalid container ID", "container", containerID)
		http.Error(w, "Invalid container ID", http.StatusBadRequest)

Fix: Check the after return value and reject requests with extra segments.

-	containerID, _, _ := strings.Cut(strings.TrimLeft(r.URL.Path, "/"), "/")
+	containerID, after, _ := strings.Cut(strings.TrimLeft(r.URL.Path, "/"), "/")
+	if after != "" {
+		http.Error(w, "Invalid path", http.StatusBadRequest)
+		return
+	}
3. WebSocket close frame may not reach client (suggestion, gap) Gemini

The deferred conn.Close() runs immediately after the close-frame write without waiting for the client to echo the close frame. Browser clients may see 1006 Abnormal Closure instead of the clean 1000 Normal Closure. Acceptable for a mock endpoint, but a brief read loop after sending the close message would produce cleaner client-side behavior.

	defer conn.Close()
	defer func() {
		message := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "Closing connection")
		err := conn.WriteControl(websocket.CloseMessage, message, time.Now().Add(time.Second))
		if err != nil {
			log.V(5).Info("Failed to close WebSocket", "error", err)
		}
	}()

Design Observations

Strengths


Testing Assessment

Coverage is adequate for a mock endpoint:

  1. No test exercises the endpoint with a browser-style Origin header. The BATS tests use curl, which omits Origin, so they miss the failure described in Important Finding 1.
  2. Container with nil labels (Status.Labels is nil) is untested, but safe — maps.Keys(nil) returns an empty iterator.
  3. Empty container ID is implicitly covered by the hex-character regex requiring at least one character (+).

Documentation Assessment

No documentation changes needed. This is a mock-only endpoint for internal development.


Agent Performance Retro

Claude

Codex

Gemini

Summary

MetricClaude Opus 4.6Codex GPT 5.4Gemini 3.1 Pro
Duration8:342:365:16
Critical000
Important112
Suggestion102
Design observations410
False positives000
Unique insights002
Files reviewed555
Coverage misses010

Gemini delivered the most comprehensive review, finding all four consolidated issues including two unique suggestions. Claude provided strong design observations and testing assessment. Codex was fast but shallow, contributing no unique findings. All three agents converged on the highest-value finding (WebSocket origin check), validating the multi-agent approach.