diff --git a/b0esche_cloud/lib/widgets/account_settings_dialog.dart b/b0esche_cloud/lib/widgets/account_settings_dialog.dart index 944de1e..e40581b 100644 --- a/b0esche_cloud/lib/widgets/account_settings_dialog.dart +++ b/b0esche_cloud/lib/widgets/account_settings_dialog.dart @@ -131,7 +131,9 @@ class _AccountSettingsDialogState extends State { _avatarUrl = response['avatarUrl'] as String?; final authState = context.read().state; if (authState is AuthAuthenticated && _avatarUrl != null) { - _avatarUrl = "$_avatarUrl&token=${authState.token}"; + if (!_avatarUrl!.contains('token=')) { + _avatarUrl = "$_avatarUrl&token=${authState.token}"; + } } if (_currentUser != null && _avatarUrl != null) { final updatedUser = _currentUser!.copyWith( @@ -412,8 +414,10 @@ class _AccountSettingsDialogState extends State { if (authState is AuthAuthenticated) { _currentUser = authState.user; _avatarUrl = _currentUser?.avatarUrl; - if (_avatarUrl != null) { - _avatarUrl = "$_avatarUrl&token=${authState.token}"; + if (_avatarUrl != null && authState.token.isNotEmpty) { + if (!_avatarUrl!.contains('token=')) { + _avatarUrl = "$_avatarUrl&token=${authState.token}"; + } } } diff --git a/go_cloud/internal/http/routes.go b/go_cloud/internal/http/routes.go index 7cc4717..0c3fdfb 100644 --- a/go_cloud/internal/http/routes.go +++ b/go_cloud/internal/http/routes.go @@ -6,6 +6,7 @@ import ( "context" "database/sql" "encoding/json" + stderrors "errors" "fmt" "io" "log" @@ -4185,18 +4186,33 @@ func getUserAvatarHandler(w http.ResponseWriter, r *http.Request, db *database.D return } remotePath := strings.TrimPrefix(*avatarURL, client.BaseURL+"/") - resp, err := client.Download(r.Context(), remotePath, "") + + // Use a short timeout to avoid hanging behind proxies; return 404 if WebDAV is slow or times out + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + + resp, err := client.Download(ctx, remotePath, "") if err != nil { + // If download timed out or gateway returned 504, treat as "no avatar" so frontend shows default icon + if strings.Contains(err.Error(), "504") || stderrors.Is(err, context.DeadlineExceeded) { + fmt.Printf("[ERROR] Avatar download timeout for remotePath=%s: %v\n", remotePath, err) + w.WriteHeader(http.StatusNotFound) + return + } + errors.LogError(r, err, "Failed to download avatar") errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError) return } defer resp.Body.Close() - // Copy headers + // Copy headers but ensure sensible caching for k, v := range resp.Header { w.Header()[k] = v } + // Opt into short caching; client-side cache is also versioned via v= + w.Header().Set("Cache-Control", "public, max-age=300") + w.WriteHeader(resp.StatusCode) io.Copy(w, resp.Body) }