Skip to content
Merged
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
16 changes: 14 additions & 2 deletions pkg/api/handlers/feedback.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ import (
// githubAPITimeout is the timeout for HTTP requests to the GitHub API.
const githubAPITimeout = 10 * time.Second

// screenshotUploadTimeout is a longer timeout for uploading base64 screenshots
// to GitHub via the Contents API, which can be slow for large images.
const screenshotUploadTimeout = 60 * time.Second

// prCacheTTL is how long cached PR data is considered fresh.
const prCacheTTL = 5 * time.Minute

Expand Down Expand Up @@ -1659,9 +1663,15 @@ func (h *FeedbackHandler) uploadScreenshotToGitHub(repoOwner, repoName, requestI
ext = "webp"
}

// The base64 content (GitHub Contents API expects raw base64, no wrapping)
// The base64 content (GitHub Contents API expects raw base64, no wrapping).
// Browsers may omit trailing '=' padding, so we normalize first.
b64Content := parts[1]

// Add padding if missing — base64 requires length to be a multiple of 4
if remainder := len(b64Content) % 4; remainder != 0 {
b64Content += strings.Repeat("=", 4-remainder)
}

// Validate that the base64 content is actually valid
if _, err := base64.StdEncoding.DecodeString(b64Content); err != nil {
return "", fmt.Errorf("invalid base64 content: %w", err)
Expand All @@ -1688,7 +1698,9 @@ func (h *FeedbackHandler) uploadScreenshotToGitHub(repoOwner, repoName, requestI
req.Header.Set("Accept", "application/vnd.github.v3+json")
req.Header.Set("Content-Type", "application/json")

resp, err := h.httpClient.Do(req)
// Use a longer timeout for screenshot uploads (large base64 payloads)
uploadClient := &http.Client{Timeout: screenshotUploadTimeout}
resp, err := uploadClient.Do(req)
Comment on lines +1701 to +1703
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Creating a new http.Client here bypasses any custom configuration on h.httpClient (custom Transport, proxies, tracing, keep-alive tuning) and makes timeout behavior inconsistent across GitHub calls. Prefer keeping a single configured client and using a per-request timeout via context.WithTimeout (and req = req.WithContext(ctx) / http.NewRequestWithContext), or if a separate client is required, initialize it once (e.g., on the handler struct) reusing the same Transport as h.httpClient and only changing the timeout.

Copilot uses AI. Check for mistakes.
if err != nil {
return "", err
}
Expand Down
3 changes: 3 additions & 0 deletions pkg/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,13 @@ func NewServer(cfg Config) (*Server, error) {
middleware.InitTokenRevocation(db)

// Create Fiber app
// feedbackBodyLimit allows base64-encoded screenshot uploads (up to ~20 MB)
const feedbackBodyLimit = 20 * 1024 * 1024
app := fiber.New(fiber.Config{
ErrorHandler: customErrorHandler,
ReadBufferSize: 16384,
WriteBufferSize: 16384,
BodyLimit: feedbackBodyLimit,
Comment on lines +197 to +203
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Setting BodyLimit on the global Fiber app raises the max body size for all endpoints, which can increase memory pressure and amplify DoS risk from large request bodies unrelated to feedback. If only feedback endpoints need the larger limit, prefer applying a body-limit middleware scoped to the feedback routes (e.g., Fiber bodylimit middleware on a route group) while keeping a smaller global default, and/or pair this with rate limiting on the feedback submission endpoint.

Copilot uses AI. Check for mistakes.
ReadTimeout: 30 * time.Second,
WriteTimeout: 5 * time.Minute, // large static assets on slow networks
IdleTimeout: 2 * time.Minute,
Expand Down
1 change: 0 additions & 1 deletion web/src/components/cards/CardWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,6 @@ function InfoTooltip({ text }: { text: string }) {
onMouseLeave={() => setIsVisible(false)}
className="p-0.5 rounded text-muted-foreground/50 hover:text-muted-foreground transition-colors"
aria-label={t('cardWrapper.cardInfo')}
title={t('cardWrapper.cardInfo')}
>
<Info className="w-3.5 h-3.5" />
</button>
Expand Down
6 changes: 4 additions & 2 deletions web/src/components/feedback/FeatureRequestModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import { useAuth } from '../../lib/auth'
import { useRewards } from '../../hooks/useRewards'
import { BACKEND_DEFAULT_URL, STORAGE_KEY_TOKEN, DEMO_TOKEN_VALUE, FETCH_DEFAULT_TIMEOUT_MS, COPY_FEEDBACK_TIMEOUT_MS } from '../../lib/constants'
import { FEEDBACK_UPLOAD_TIMEOUT_MS } from '../../lib/constants/network'
import { emitLinkedInShare } from '../../lib/analytics'
import { isDemoModeForced } from '../../lib/demoMode'
import { useToast } from '../ui/Toast'
Expand Down Expand Up @@ -338,13 +339,14 @@ export function FeatureRequestModal({ isOpen, onClose, initialTab, initialReques
const screenshotDataURIs = screenshots.map(s => s.preview)

try {
const hasScreenshots = screenshotDataURIs.length > 0
const result = await createRequest({
title: extractedTitle,
description: extractedDesc,
request_type: requestType,
target_repo: targetRepo,
...(screenshotDataURIs.length > 0 && { screenshots: screenshotDataURIs }),
})
...(hasScreenshots && { screenshots: screenshotDataURIs }),
}, hasScreenshots ? { timeout: FEEDBACK_UPLOAD_TIMEOUT_MS } : undefined)
setSuccess({ issueUrl: result.github_issue_url })
// Keep the success state visible for 5s so users can read the confirmation and open the issue before switching to the Updates tab
setTimeout(() => {
Expand Down
6 changes: 4 additions & 2 deletions web/src/components/feedback/FeedbackModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { useToast } from '../ui/Toast'
import { emitFeedbackSubmitted, emitLinkedInShare } from '../../lib/analytics'
import { useBranding } from '../../hooks/useBranding'
import { FETCH_DEFAULT_TIMEOUT_MS, COPY_FEEDBACK_TIMEOUT_MS } from '../../lib/constants'
import { FEEDBACK_UPLOAD_TIMEOUT_MS } from '../../lib/constants/network'
import { useFeatureRequests } from '../../hooks/useFeatureRequests'
import { useAuth } from '../../lib/auth'

Expand Down Expand Up @@ -158,13 +159,14 @@ export function FeedbackModal({ isOpen, onClose, initialType = 'feature' }: Feed
// Submit via backend API — creates GitHub issue directly using the
// server-side token. No GitHub login required from the user.
// Screenshots are uploaded server-side and embedded as images.
const hasScreenshots = screenshotDataURIs.length > 0
const result = await createRequest({
title: title.trim(),
description: description.trim(),
request_type: type,
target_repo: 'console',
...(screenshotDataURIs.length > 0 && { screenshots: screenshotDataURIs }),
})
...(hasScreenshots && { screenshots: screenshotDataURIs }),
}, hasScreenshots ? { timeout: FEEDBACK_UPLOAD_TIMEOUT_MS } : undefined)

emitFeedbackSubmitted(type)

Expand Down
4 changes: 2 additions & 2 deletions web/src/hooks/useFeatureRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,10 +300,10 @@ export function useFeatureRequests(currentUserId?: string) {
setIsRefreshing(false)
}, [loadRequests])

const createRequest = useCallback(async (input: CreateFeatureRequestInput) => {
const createRequest = useCallback(async (input: CreateFeatureRequestInput, options?: { timeout?: number }) => {
try {
setIsSubmitting(true)
const { data } = await api.post<FeatureRequest>('/api/feedback/requests', input)
const { data } = await api.post<FeatureRequest>('/api/feedback/requests', input, options)
Comment on lines +303 to +306
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

The new options parameter is typed as { timeout?: number } but is passed directly to api.post(...) as the request config. If api is an Axios-like client (or a custom wrapper that supports more config fields), this type is overly restrictive and can lead to unsafe/incorrect usage (callers can’t pass other supported config, and the shape may not match what api.post expects). Prefer typing options to the actual config type used by api.post (e.g., AxiosRequestConfig or your project’s ApiRequestOptions) and/or narrowing it at the call site instead of widening the hook API.

Copilot uses AI. Check for mistakes.
setRequests(prev => [data, ...prev])
return data
} finally {
Expand Down
5 changes: 5 additions & 0 deletions web/src/lib/constants/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ export const FETCH_DEFAULT_TIMEOUT_MS = 10_000
/** Timeout for fetch() calls to external services (GitHub API, registries, etc.) */
export const FETCH_EXTERNAL_TIMEOUT_MS = 15_000

/** Extended timeout for feedback submissions with screenshot uploads (90 seconds).
* Screenshots are uploaded server-side to GitHub via the Contents API, which can
* be slow for multiple large images. */
export const FEEDBACK_UPLOAD_TIMEOUT_MS = 90_000

// ============================================================================
// UI Feedback Timeouts
// ============================================================================
Expand Down
Loading