Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
449 changes: 449 additions & 0 deletions components/backend/handlers/gerrit_auth.go

Large diffs are not rendered by default.

450 changes: 450 additions & 0 deletions components/backend/handlers/gerrit_auth_test.go

Large diffs are not rendered by default.

122 changes: 122 additions & 0 deletions components/backend/handlers/integration_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
"time"

"github.com/gin-gonic/gin"
Expand Down Expand Up @@ -143,6 +145,126 @@ func ValidateGoogleToken(ctx context.Context, accessToken string) (bool, error)
return resp.StatusCode == http.StatusOK, nil
}

// ValidateGerritToken checks if Gerrit credentials are valid
// Uses /a/accounts/self endpoint which accepts Basic Auth or Cookie-based auth
// Gerrit REST API responses are prefixed with )]}' (XSSI protection)
func ValidateGerritToken(ctx context.Context, gerritURL, authMethod, username, httpToken, gitcookiesContent string) (bool, error) {
if gerritURL == "" {
return false, fmt.Errorf("Gerrit URL is required")
}

client := &http.Client{Timeout: 15 * time.Second}
apiURL := fmt.Sprintf("%s/a/accounts/self", gerritURL)

req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil)
if err != nil {
return false, fmt.Errorf("failed to create request")
}
req.Header.Set("Accept", "application/json")

switch authMethod {
case "http_basic":
if username == "" || httpToken == "" {
return false, fmt.Errorf("username and HTTP token are required for HTTP basic auth")
}
req.SetBasicAuth(username, httpToken)

case "git_cookies":
if gitcookiesContent == "" {
return false, fmt.Errorf("gitcookies content is required")
}
// Parse gitcookies content to extract cookie for the target host
cookie := parseGitcookies(gerritURL, gitcookiesContent)
if cookie == "" {
return false, fmt.Errorf("no matching cookie found for host in gitcookies content")
}
req.Header.Set("Cookie", cookie)

default:
return false, fmt.Errorf("unsupported auth method: %s", authMethod)
}

resp, err := client.Do(req)
if err != nil {
// Don't wrap error - could leak credentials from request details
return false, fmt.Errorf("request failed")
}
defer resp.Body.Close()

// 200 = valid, 401/403 = invalid
if resp.StatusCode == http.StatusOK {
return true, nil
}
if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
return false, nil
}

// Other status codes - can't validate, assume valid to avoid false negatives
return true, nil
}

// parseGitcookies extracts the cookie value for a given Gerrit URL from gitcookies content.
// Gitcookies format: host\tFALSE\t/\tTRUE\t2147483647\to\tvalue
func parseGitcookies(gerritURL, content string) string {
parsed, err := url.Parse(gerritURL)
if err != nil {
return ""
}
host := parsed.Hostname()

lines := strings.Split(content, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
fields := strings.Split(line, "\t")
if len(fields) >= 7 {
cookieHost := strings.TrimPrefix(fields[0], ".")
if cookieHost == host || strings.HasSuffix(host, "."+cookieHost) {
return fields[5] + "=" + fields[6]
}
}
}
return ""
}

// TestGerritConnection handles POST /api/auth/gerrit/test
// Tests Gerrit credentials without saving them
func TestGerritConnection(c *gin.Context) {
var req struct {
URL string `json:"url" binding:"required"`
AuthMethod string `json:"authMethod" binding:"required"`
Username string `json:"username"`
HTTPToken string `json:"httpToken"`
GitcookiesContent string `json:"gitcookiesContent"`
}

if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

// Validate URL (SSRF protection)
if err := validateGerritURL(req.URL); err != nil {
c.JSON(http.StatusOK, gin.H{"valid": false, "error": fmt.Sprintf("Invalid Gerrit URL: %s", err.Error())})
return
}

valid, err := validateGerritTokenFn(c.Request.Context(), req.URL, req.AuthMethod, req.Username, req.HTTPToken, req.GitcookiesContent)
if err != nil {
c.JSON(http.StatusOK, gin.H{"valid": false, "error": err.Error()})
return
}

if !valid {
c.JSON(http.StatusOK, gin.H{"valid": false, "error": "Invalid credentials"})
return
}

c.JSON(http.StatusOK, gin.H{"valid": true, "message": "Gerrit connection successful"})
}

// TestJiraConnection handles POST /api/auth/jira/test
// Tests Jira credentials without saving them
func TestJiraConnection(c *gin.Context) {
Expand Down
33 changes: 33 additions & 0 deletions components/backend/handlers/integrations_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"log"
"net/http"
"sort"

"github.com/gin-gonic/gin"
)
Expand Down Expand Up @@ -39,6 +40,9 @@ func GetIntegrationsStatus(c *gin.Context) {
// GitLab status
response["gitlab"] = getGitLabStatusForUser(ctx, userID)

// Gerrit status
response["gerrit"] = getGerritStatusForUser(ctx, userID)

// MCP server credentials status
response["mcpServers"] = getMCPServerStatusForUser(ctx, userID)

Expand All @@ -47,6 +51,7 @@ func GetIntegrationsStatus(c *gin.Context) {

// Helper functions to get individual integration statuses

// getGitHubStatusForUser returns the GitHub integration status (App + PAT) for a user.
func getGitHubStatusForUser(ctx context.Context, userID string) gin.H {
log.Printf("getGitHubStatusForUser: querying status for user=%s", userID)
status := gin.H{
Expand Down Expand Up @@ -90,6 +95,7 @@ func getGitHubStatusForUser(ctx context.Context, userID string) gin.H {
return status
}

// getGoogleStatusForUser returns the Google OAuth integration status for a user.
func getGoogleStatusForUser(ctx context.Context, userID string) gin.H {
creds, err := GetGoogleCredentials(ctx, userID)
if err != nil || creds == nil {
Expand All @@ -108,6 +114,7 @@ func getGoogleStatusForUser(ctx context.Context, userID string) gin.H {
}
}

// getJiraStatusForUser returns the Jira integration status for a user.
func getJiraStatusForUser(ctx context.Context, userID string) gin.H {
creds, err := GetJiraCredentials(ctx, userID)
if err != nil || creds == nil {
Expand All @@ -128,6 +135,32 @@ func getJiraStatusForUser(ctx context.Context, userID string) gin.H {
}
}

// getGerritStatusForUser returns the Gerrit integration status with all connected instances for a user.
func getGerritStatusForUser(ctx context.Context, userID string) gin.H {
instances, err := listGerritCredentials(ctx, userID)
if err != nil || len(instances) == 0 {
return gin.H{"instances": []gin.H{}}
}

sort.Slice(instances, func(i, j int) bool {
return instances[i].InstanceName < instances[j].InstanceName
})

result := make([]gin.H, 0, len(instances))
for _, creds := range instances {
result = append(result, gin.H{
"connected": true,
"instanceName": creds.InstanceName,
"url": creds.URL,
"authMethod": creds.AuthMethod,
"updatedAt": creds.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
})
}

return gin.H{"instances": result}
}

// getGitLabStatusForUser returns the GitLab integration status for a user.
func getGitLabStatusForUser(ctx context.Context, userID string) gin.H {
creds, err := GetGitLabCredentials(ctx, userID)
if err != nil || creds == nil {
Expand Down
49 changes: 49 additions & 0 deletions components/backend/handlers/runtime_credentials.go
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,55 @@ func fetchGitHubUserIdentity(ctx context.Context, token string) (userName, email
return userName, email
}

// GetGerritCredentialsForSession handles GET /api/projects/:project/agentic-sessions/:session/credentials/gerrit
// Returns all Gerrit instances' credentials for the session's user
func GetGerritCredentialsForSession(c *gin.Context) {
project := c.Param("projectName")
session := c.Param("sessionName")

reqK8s, reqDyn := GetK8sClientsForRequest(c)
if reqK8s == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or missing token"})
return
}

effectiveUserID, ok := enforceCredentialRBAC(c, reqK8s, reqDyn, project, session)
if !ok {
return
}

// Get all Gerrit instances for the user
instances, err := listGerritCredentials(c.Request.Context(), effectiveUserID)
if err != nil {
log.Printf("Failed to get Gerrit credentials for user %s: %v", effectiveUserID, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get Gerrit credentials"})
return
}

if len(instances) == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Gerrit credentials not configured"})
return
}

result := make([]gin.H, 0, len(instances))
for _, creds := range instances {
instance := gin.H{
"instanceName": creds.InstanceName,
"url": creds.URL,
"authMethod": creds.AuthMethod,
}
if creds.AuthMethod == "http_basic" {
instance["username"] = creds.Username
instance["httpToken"] = creds.HTTPToken
} else if creds.AuthMethod == "git_cookies" {
instance["gitcookiesContent"] = creds.GitcookiesContent
}
result = append(result, instance)
}

c.JSON(http.StatusOK, gin.H{"instances": result})
}

// fetchGitLabUserIdentity fetches user name and email from GitLab API
// Returns the user's name and email for git config
func fetchGitLabUserIdentity(ctx context.Context, token, instanceURL string) (userName, email string) {
Expand Down
12 changes: 9 additions & 3 deletions components/backend/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ func registerRoutes(r *gin.Engine) {
api := r.Group("/api")
{
// Public endpoints (no auth required)
api.GET("/version", handlers.GetVersion)
api.GET("/workflows/ootb", handlers.ListOOTBWorkflows)
// Global runner-types endpoint (no workspace overrides — for admin pages)
api.GET("/runner-types", handlers.GetRunnerTypesGlobal)
Expand Down Expand Up @@ -83,6 +84,7 @@ func registerRoutes(r *gin.Engine) {
projectGroup.GET("/agentic-sessions/:sessionName/credentials/google", handlers.GetGoogleCredentialsForSession)
projectGroup.GET("/agentic-sessions/:sessionName/credentials/jira", handlers.GetJiraCredentialsForSession)
projectGroup.GET("/agentic-sessions/:sessionName/credentials/gitlab", handlers.GetGitLabTokenForSession)
projectGroup.GET("/agentic-sessions/:sessionName/credentials/gerrit", handlers.GetGerritCredentialsForSession)
projectGroup.GET("/agentic-sessions/:sessionName/credentials/mcp/:serverName", handlers.GetMCPCredentialsForSession)

// Session export
Expand Down Expand Up @@ -159,6 +161,13 @@ func registerRoutes(r *gin.Engine) {
api.DELETE("/auth/gitlab/disconnect", handlers.DisconnectGitLabGlobal)
api.POST("/auth/gitlab/test", handlers.TestGitLabConnection)

// Cluster-level Gerrit (user-scoped, multi-instance)
api.POST("/auth/gerrit/connect", handlers.ConnectGerrit)
api.POST("/auth/gerrit/test", handlers.TestGerritConnection)
api.GET("/auth/gerrit/instances", handlers.ListGerritInstances)
api.GET("/auth/gerrit/:instanceName/status", handlers.GetGerritStatus)
api.DELETE("/auth/gerrit/:instanceName/disconnect", handlers.DisconnectGerrit)

// Generic MCP server credentials (user-scoped)
api.POST("/auth/mcp/:serverName/connect", handlers.ConnectMCPServer)
api.GET("/auth/mcp/:serverName/status", handlers.GetMCPServerStatus)
Expand All @@ -167,9 +176,6 @@ func registerRoutes(r *gin.Engine) {
// Cluster info endpoint (public, no auth required)
api.GET("/cluster-info", handlers.GetClusterInfo)

// Version endpoint (public, no auth required)
api.GET("/version", handlers.GetVersion)

// LDAP search endpoints (cluster-scoped, auth-required)
api.GET("/ldap/users", handlers.SearchLDAPUsers)
api.GET("/ldap/users/:uid", handlers.GetLDAPUser)
Expand Down
1 change: 1 addition & 0 deletions components/backend/tests/constants/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const (
LabelPermissions = "permissions"
LabelProjects = "projects"
LabelGitHubAuth = "github-auth"
LabelGerritAuth = "gerrit-auth"
LabelGitLabAuth = "gitlab-auth"
LabelSessions = "sessions"
LabelContent = "content"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { BACKEND_URL } from '@/lib/config'
import { buildForwardHeadersAsync } from '@/lib/auth'

export async function DELETE(
request: Request,
{ params }: { params: Promise<{ instanceName: string }> }
) {
const { instanceName } = await params
const safeInstanceName = encodeURIComponent(instanceName)
const headers = await buildForwardHeadersAsync(request)

const resp = await fetch(`${BACKEND_URL}/auth/gerrit/${safeInstanceName}/disconnect`, {
method: 'DELETE',
headers,
})

const data = await resp.text()
return new Response(data, { status: resp.status, headers: { 'Content-Type': 'application/json' } })
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { BACKEND_URL } from '@/lib/config'
import { buildForwardHeadersAsync } from '@/lib/auth'

export async function GET(
request: Request,
{ params }: { params: Promise<{ instanceName: string }> }
) {
const { instanceName } = await params
const safeInstanceName = encodeURIComponent(instanceName)
const headers = await buildForwardHeadersAsync(request)

const resp = await fetch(`${BACKEND_URL}/auth/gerrit/${safeInstanceName}/status`, {
method: 'GET',
headers,
})

const data = await resp.text()
return new Response(data, { status: resp.status, headers: { 'Content-Type': 'application/json' } })
}
16 changes: 16 additions & 0 deletions components/frontend/src/app/api/auth/gerrit/connect/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { BACKEND_URL } from '@/lib/config'
import { buildForwardHeadersAsync } from '@/lib/auth'

export async function POST(request: Request) {
const headers = await buildForwardHeadersAsync(request)
const body = await request.text()

const resp = await fetch(`${BACKEND_URL}/auth/gerrit/connect`, {
method: 'POST',
headers,
body,
})

const data = await resp.text()
return new Response(data, { status: resp.status, headers: { 'Content-Type': 'application/json' } })
Comment on lines +8 to +15
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify Gerrit proxy routes include timeout signal and local catch handling.
rg -n --type=ts -C2 'fetch\(`\$\{BACKEND_URL\}/auth/gerrit' components/frontend/src/app/api/auth/gerrit
rg -n --type=ts -C2 'signal\s*:' components/frontend/src/app/api/auth/gerrit
rg -n --type=ts -C2 'catch\s*\(' components/frontend/src/app/api/auth/gerrit

Repository: ambient-code/platform

Length of output: 2600


🏁 Script executed:

cat -n components/frontend/src/app/api/auth/gerrit/connect/route.ts

Repository: ambient-code/platform

Length of output: 669


🏁 Script executed:

# Check if similar unprotected fetch patterns exist in other auth routes
rg -n --type=ts 'await fetch\(`\$\{BACKEND_URL\}' components/frontend/src/app/api/auth --no-heading | head -20

Repository: ambient-code/platform

Length of output: 2626


🏁 Script executed:

# Check for existing timeout patterns or utilities
rg -n --type=ts 'AbortController|setTimeout.*timeout|TIMEOUT|Abort' components/frontend/src/lib --type ts | head -10

Repository: ambient-code/platform

Length of output: 149


Add timeout + network-failure handling for the backend proxy call.

Line 8 issues an upstream fetch without timeout/abort handling; a hung backend call can stall this route indefinitely.

Proposed fix
 export async function POST(request: Request) {
   const headers = await buildForwardHeadersAsync(request)
   const body = await request.text()
 
-  const resp = await fetch(`${BACKEND_URL}/auth/gerrit/connect`, {
-    method: 'POST',
-    headers,
-    body,
-  })
-
-  const data = await resp.text()
-  return new Response(data, { status: resp.status, headers: { 'Content-Type': 'application/json' } })
+  const controller = new AbortController()
+  const timeout = setTimeout(() => controller.abort(), 10_000)
+  try {
+    const resp = await fetch(`${BACKEND_URL}/auth/gerrit/connect`, {
+      method: 'POST',
+      headers,
+      body,
+      signal: controller.signal,
+    })
+
+    const data = await resp.text()
+    return new Response(data, { status: resp.status, headers: { 'Content-Type': 'application/json' } })
+  } catch {
+    return Response.json({ error: 'Upstream Gerrit connect request failed' }, { status: 504 })
+  } finally {
+    clearTimeout(timeout)
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/app/api/auth/gerrit/connect/route.ts` around lines 8
- 15, The fetch call to BACKEND_URL in the route (the const resp = await
fetch(...) block) needs timeout and network-failure handling: wrap the fetch
with an AbortController, set a timer (e.g., 5–10s) that calls
controller.abort(), clear the timer after fetch resolves, and wrap the await
fetch in try/catch to handle AbortError and other network errors; on
timeout/abort return a Response with an appropriate status (e.g., 504 Gateway
Timeout) and JSON error body, and on other network errors return a 502/500 JSON
response—ensure you still preserve resp.status and Content-Type:
application/json for successful responses.

}
14 changes: 14 additions & 0 deletions components/frontend/src/app/api/auth/gerrit/instances/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { BACKEND_URL } from '@/lib/config'
import { buildForwardHeadersAsync } from '@/lib/auth'

export async function GET(request: Request) {
const headers = await buildForwardHeadersAsync(request)

const resp = await fetch(`${BACKEND_URL}/auth/gerrit/instances`, {
method: 'GET',
headers,
})

const data = await resp.text()
return new Response(data, { status: resp.status, headers: { 'Content-Type': 'application/json' } })
Comment on lines +7 to +13
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the actual file and its context
cat -n components/frontend/src/app/api/auth/gerrit/instances/route.ts

Repository: ambient-code/platform

Length of output: 609


🏁 Script executed:

# Search for other auth/gerrit related routes to find the "robust proxy endpoints"
fd -t f "route.ts" components/frontend/src/app/api/auth/ | head -20

Repository: ambient-code/platform

Length of output: 1330


🏁 Script executed:

# Search for examples of timeout handling in API routes
rg -A 5 -B 5 "timeout|AbortController" components/frontend/src/app/api/ --type ts

Repository: ambient-code/platform

Length of output: 47


🏁 Script executed:

# Search more broadly for fetch calls with timeout handling
rg "fetch" components/frontend/src/app/api/ --type ts -A 3 | head -100

Repository: ambient-code/platform

Length of output: 8197


🏁 Script executed:

# Look for AbortSignal or timeout patterns in lib or utils
rg -r "AbortSignal|AbortController|timeout" components/frontend/src/lib/ --type ts | head -20

Repository: ambient-code/platform

Length of output: 47


🏁 Script executed:

# Search for the word "robust" or "proxy" in the API directory
rg "proxy|robust" components/frontend/src/app/api/ --type ts

Repository: ambient-code/platform

Length of output: 1909


🏁 Script executed:

# Check if there's a middleware or config that might handle timeouts globally
find components/frontend/src -name "*.ts" -path "*/middleware*" -o -name "*.ts" -path "*/config*" | xargs grep -l "timeout" 2>/dev/null | head -10

Repository: ambient-code/platform

Length of output: 47


🏁 Script executed:

# Look for custom fetch wrappers or utilities
fd -t f "fetch|http|proxy" components/frontend/src/lib/ --type ts | xargs grep -l "fetch" 2>/dev/null | head -10

Repository: ambient-code/platform

Length of output: 234


🏁 Script executed:

# Check if there's any middleware
fd -t f "middleware" components/frontend/src/ --type ts | head -10

Repository: ambient-code/platform

Length of output: 234


🏁 Script executed:

# Look at one of the endpoints with error handling more closely to understand the pattern
cat -n components/frontend/src/app/api/version/route.ts

Repository: ambient-code/platform

Length of output: 988


🏁 Script executed:

# Search for any mention of signal or timeout in the entire codebase
rg "signal|Signal" components/frontend/src/app/api/ --type ts -B 2 -A 2 | head -50

Repository: ambient-code/platform

Length of output: 759


🏁 Script executed:

# Search for middleware files more broadly
find components/frontend/src -name "*.ts" | xargs grep -l "middleware" 2>/dev/null | head -10

Repository: ambient-code/platform

Length of output: 210


🏁 Script executed:

# Check if there's a next.config or similar configuration
find components/frontend -name "next.config*" -o -name "tsconfig*" | xargs cat 2>/dev/null | head -50

Repository: ambient-code/platform

Length of output: 1217


🏁 Script executed:

# Look at the gerrit instances route in more detail and its surrounding context
cat -n components/frontend/src/app/api/auth/gerrit/instances/route.ts

Repository: ambient-code/platform

Length of output: 609


🏁 Script executed:

# Check other gerrit routes to see if they have error handling
cat -n components/frontend/src/app/api/auth/gerrit/connect/route.ts

Repository: ambient-code/platform

Length of output: 669


🏁 Script executed:

# Check if Next.js has built-in timeout mechanisms through request.signal
rg "request\.signal" components/frontend/src/app/api/ -A 2 -B 2 | head -30

Repository: ambient-code/platform

Length of output: 47


🏁 Script executed:

# Look at a more complex route to see if there's a pattern for handling long-running operations
cat -n components/frontend/src/app/api/projects/\[name\]/agentic-sessions/\[sessionName\]/agui/run/route.ts | head -80

Repository: ambient-code/platform

Length of output: 1930


🏁 Script executed:

# Check if there's any documentation or patterns for handling timeouts in Next.js routes
rg "fetch.*signal|AbortSignal|timeout" components/frontend/src --type file | head -20

Repository: ambient-code/platform

Length of output: 93


🏁 Script executed:

# Search for any environment-based timeout configuration
rg "timeout|TIMEOUT" components/frontend/src --type file 2>/dev/null | head -20

Repository: ambient-code/platform

Length of output: 47


🏁 Script executed:

# Check if any of the auth routes have error handling
for file in components/frontend/src/app/api/auth/gerrit/*.ts components/frontend/src/app/api/auth/gerrit/*/route.ts; do
  if [ -f "$file" ]; then
    echo "=== $file ==="
    cat "$file"
  fi
done

Repository: ambient-code/platform

Length of output: 1700


🏁 Script executed:

# Check the github auth routes to see if they have a different pattern
cat -n components/frontend/src/app/api/auth/github/user/callback/route.ts | head -50

Repository: ambient-code/platform

Length of output: 1004


🏁 Script executed:

# Final check: confirm if any route has both timeout and error handling combined
rg -A 10 "fetch.*{" components/frontend/src/app/api/ | rg -B 5 -A 5 "signal|AbortController|timeout" | head -20

Repository: ambient-code/platform

Length of output: 144


🏁 Script executed:

# Final verification: check all fetch calls for signal/timeout handling
rg "fetch\(" components/frontend/src/app/api/ -A 5 | rg "signal\|AbortController" -B 3 -A 3

Repository: ambient-code/platform

Length of output: 47


Add timeout and error handling for the backend fetch call.

This route lacks both timeout protection and error handling. If the backend stalls, the route will hang indefinitely, making the endpoint unresponsive. Add AbortSignal with a reasonable timeout (e.g., 30 seconds) to the fetch request, and wrap the fetch in a try-catch block to handle errors gracefully.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/app/api/auth/gerrit/instances/route.ts` around lines
7 - 13, Wrap the backend fetch in a try-catch and add an AbortController-based
timeout (e.g., 30s) so the call using
fetch(`${BACKEND_URL}/auth/gerrit/instances`, { method: 'GET', headers }) cannot
hang; create an AbortController, pass controller.signal to fetch, set a timer to
call controller.abort() after 30000ms, clear the timer on success, and in the
catch return a sensible Response (e.g., 504 Gateway Timeout for an abort or 500
for other errors) with a JSON error message while preserving appropriate headers
and status handling for resp/status.

}
Loading