|
| 1 | +# Deco.cx Error Handling & Security Patterns |
| 2 | + |
| 3 | +**Version:** 1.0 |
| 4 | +**Date:** September 2025 |
| 5 | +**Target Audience:** AI Coding Agents & Developers |
| 6 | +**Parent Guide:** [deco-cx-store-migration-plan.md](./deco-cx-store-migration-plan.md) |
| 7 | + |
| 8 | +This document contains real-world error handling patterns and security fixes derived from analyzing recent commits in production deco.cx stores. |
| 9 | + |
| 10 | +## 🛡️ Security & Input Sanitization Patterns |
| 11 | + |
| 12 | +### 1. Search Parameter Sanitization |
| 13 | + |
| 14 | +**Real Issue Found:** VTEX search API was receiving malformed query strings and embedded data URIs causing 500 errors. |
| 15 | + |
| 16 | +**Stack Trace Example:** |
| 17 | +``` |
| 18 | +SearchResult: failed to load productListingPage err=Request failed with status 500 |
| 19 | +url=https://example-store.deco.site/search?q=product+data:text/html,<script>... |
| 20 | +facets= sort= count=24 query=product+data:text/html,<script>... page=0 fuzzy=enabled hideUnavailableItems=true |
| 21 | +``` |
| 22 | + |
| 23 | +**Solution Pattern:** |
| 24 | +```typescript |
| 25 | +// components/search/InfiniteScroll.tsx & SearchResult.tsx |
| 26 | +const sanitizeSegment = (segment: string) => { |
| 27 | + const s = segment.trim() |
| 28 | + if (!s) return '' |
| 29 | + const lower = s.toLowerCase() |
| 30 | + // Block data URIs and javascript URLs |
| 31 | + if (lower.includes('data:') || lower.includes('javascript:')) return '' |
| 32 | + if (lower.includes('data%3a') || lower.includes('javascript%3a')) return '' |
| 33 | + if (s.length > 200) return '' |
| 34 | + return s |
| 35 | +} |
| 36 | + |
| 37 | +// Apply to URL path segments |
| 38 | +const sanitizedSegments = currentUrl.pathname |
| 39 | + .split('/') |
| 40 | + .map(sanitizeSegment) |
| 41 | + .filter((item) => item !== '') |
| 42 | + |
| 43 | +// Also sanitize query parameters |
| 44 | +const query = searchTerm.replace(/\+/g, ' ').trim(); // Convert + to spaces |
| 45 | +``` |
| 46 | + |
| 47 | +**Key Protections:** |
| 48 | +- Block `data:` and `javascript:` schemes (both raw and URL-encoded) |
| 49 | +- Limit segment length to prevent oversized payloads |
| 50 | +- Proper URL encoding for search queries |
| 51 | + |
| 52 | +### 2. Component Safety Guards |
| 53 | + |
| 54 | +**Real Issue Found:** `BannerWithTitle` component crashing with "Cannot read properties of undefined (startsWith)" |
| 55 | + |
| 56 | +**Stack Trace Pattern:** |
| 57 | +``` |
| 58 | +TypeError: Cannot read properties of undefined (reading 'startsWith') |
| 59 | + at BannerWithTitle.tsx:45:23 |
| 60 | + at renderToString (deno:ext/node/polyfills/_utils.ts:22:22) |
| 61 | +``` |
| 62 | + |
| 63 | +**Solution Pattern:** |
| 64 | +```typescript |
| 65 | +// components/ui/BannerWithTitle.tsx |
| 66 | +export default function BannerWithTitle({ |
| 67 | + mobile, |
| 68 | + desktop, |
| 69 | + title |
| 70 | +}: Props) { |
| 71 | + // Guard against undefined image sources |
| 72 | + const mobileSrc = mobile?.src |
| 73 | + const desktopSrc = desktop?.src |
| 74 | + |
| 75 | + // Provide safe fallback |
| 76 | + const fallbackSrc = mobileSrc || desktopSrc || '/static/placeholder.jpg' |
| 77 | + |
| 78 | + return ( |
| 79 | + <Picture preload> |
| 80 | + {mobileSrc && ( |
| 81 | + <Source |
| 82 | + media="(max-width: 767px)" |
| 83 | + src={mobileSrc} |
| 84 | + width={mobile.width} |
| 85 | + height={mobile.height} |
| 86 | + /> |
| 87 | + )} |
| 88 | + {desktopSrc && ( |
| 89 | + <Source |
| 90 | + media="(min-width: 768px)" |
| 91 | + src={desktopSrc} |
| 92 | + width={desktop.width} |
| 93 | + height={desktop.height} |
| 94 | + /> |
| 95 | + )} |
| 96 | + <img |
| 97 | + class="object-cover w-full h-full" |
| 98 | + loading="lazy" |
| 99 | + src={fallbackSrc} |
| 100 | + alt={title || "Banner"} |
| 101 | + /> |
| 102 | + </Picture> |
| 103 | + ) |
| 104 | +} |
| 105 | +``` |
| 106 | + |
| 107 | +## 🔄 Error Handling & Logging Patterns |
| 108 | + |
| 109 | +### 1. Structured Error Logging for External APIs |
| 110 | + |
| 111 | +**Real Implementation from production store:** |
| 112 | +```typescript |
| 113 | +// components/search/InfiniteScroll.tsx |
| 114 | +try { |
| 115 | + const page = await invoke.vtex.loaders.intelligentSearch.productListingPage({ |
| 116 | + selectedFacets: newSelectedFacets, |
| 117 | + sort: sort as SortType, |
| 118 | + count: pageInfo.recordPerPage, |
| 119 | + query, |
| 120 | + page: pageInfo.currentPage - offset, |
| 121 | + fuzzy: fuzzy ?? 'disabled', |
| 122 | + hideUnavailableItems: true, |
| 123 | + }) |
| 124 | + nextPage.value = page |
| 125 | +} catch (error) { |
| 126 | + const facetsStr = (newSelectedFacets ?? []) |
| 127 | + .map((f) => `${f.key}:${f.value}`) |
| 128 | + .join('|') |
| 129 | + |
| 130 | + // Clear the next page to prevent infinite loading |
| 131 | + nextPage.value = null |
| 132 | + |
| 133 | + // Create structured error for monitoring |
| 134 | + const err = new Error( |
| 135 | + `InfiniteScroll: failed to fetch next page err=${ |
| 136 | + (error as Error)?.message ?? 'unknown' |
| 137 | + } facets=${facetsStr} sort=${sort ?? ''} count=${pageInfo.recordPerPage} query=${ |
| 138 | + query ?? '' |
| 139 | + } page=${pageInfo.currentPage - offset} fuzzy=${ |
| 140 | + fuzzy ?? 'disabled' |
| 141 | + } hideUnavailableItems=true`, |
| 142 | + ) |
| 143 | + |
| 144 | + // Re-throw so the framework/section error boundary can capture and log it |
| 145 | + throw err |
| 146 | +} |
| 147 | +``` |
| 148 | + |
| 149 | +**Key Pattern Elements:** |
| 150 | +1. **Structured error messages** with all relevant parameters |
| 151 | +2. **State cleanup** before throwing (clear loading states) |
| 152 | +3. **Re-throw pattern** to let framework error boundaries handle logging |
| 153 | +4. **Consistent error format** across similar components |
| 154 | + |
| 155 | +### 2. Search Result Error Handling |
| 156 | + |
| 157 | +**Pattern for Search/PLP Components:** |
| 158 | +```typescript |
| 159 | +// components/search/SearchResult.tsx |
| 160 | +try { |
| 161 | + page = await ctx.invoke('vtex/loaders/intelligentSearch/productListingPage.ts', { |
| 162 | + selectedFacets: newSelectedFacets, |
| 163 | + sort: selectedSort as string, |
| 164 | + count: recordPerPage, |
| 165 | + query, |
| 166 | + page: currentPage, |
| 167 | + fuzzy: 'enabled', |
| 168 | + hideUnavailableItems: true, |
| 169 | + }) |
| 170 | +} catch (error) { |
| 171 | + const facetsStr = (newSelectedFacets ?? []) |
| 172 | + .map((f) => `${f.key}:${f.value}`) |
| 173 | + .join('|') |
| 174 | + |
| 175 | + throw new Error( |
| 176 | + `SearchResult: failed to load productListingPage err=${ |
| 177 | + (error as Error)?.message ?? 'unknown' |
| 178 | + } url=${req.url} facets=${facetsStr} sort=${ |
| 179 | + selectedSort ?? '' |
| 180 | + } count=${recordPerPage} query=${ |
| 181 | + query ?? '' |
| 182 | + } page=${currentPage} fuzzy=enabled hideUnavailableItems=true`, |
| 183 | + ) |
| 184 | +} |
| 185 | +``` |
| 186 | + |
| 187 | +### 3. UI State Management During Errors |
| 188 | + |
| 189 | +**Real Implementation Pattern:** |
| 190 | +```typescript |
| 191 | +// components/search/InfiniteScroll.tsx - Button click handler |
| 192 | +onClick={async (event) => { |
| 193 | + const target = event.currentTarget as HTMLElement |
| 194 | + target.setAttribute('data-loading', '') |
| 195 | + |
| 196 | + try { |
| 197 | + await fetchNextPage() |
| 198 | + // Only update UI state if successful |
| 199 | + if (typeof konfidencyLoader !== 'undefined') { |
| 200 | + konfidencyLoader.loadShowcase() |
| 201 | + } |
| 202 | + } finally { |
| 203 | + // Always clean up loading state, even on error |
| 204 | + target.removeAttribute('data-loading') |
| 205 | + } |
| 206 | +}} |
| 207 | +``` |
| 208 | + |
| 209 | +## 🎨 Image Optimization & Loading Patterns |
| 210 | + |
| 211 | +### 1. Missing Height Warnings Fix |
| 212 | + |
| 213 | +**Real Issue:** "Missing height. This image will NOT be optimized" logs in brand carousels |
| 214 | + |
| 215 | +**Solution Pattern:** |
| 216 | +```typescript |
| 217 | +// components/ui/InfiniteSlider.tsx |
| 218 | +{brands?.map((brand, index) => ( |
| 219 | + <div key={index} class="min-w-[120px]"> |
| 220 | + <img |
| 221 | + src={brand.src} |
| 222 | + alt={brand.alt} |
| 223 | + width={120} |
| 224 | + height={120} // ✅ Explicit height prevents optimization warnings |
| 225 | + class="object-contain" |
| 226 | + loading="lazy" |
| 227 | + /> |
| 228 | + </div> |
| 229 | +))} |
| 230 | +``` |
| 231 | + |
| 232 | +**Critical Fix Points:** |
| 233 | +- Brand carousel images need explicit square dimensions |
| 234 | +- Product images should maintain aspect ratios with explicit dimensions |
| 235 | +- Use `object-contain` for logos, `object-cover` for banners |
| 236 | + |
| 237 | +### 2. Safe Image Component Pattern |
| 238 | + |
| 239 | +**Template for Image Components:** |
| 240 | +```typescript |
| 241 | +interface ImageProps { |
| 242 | + src?: string |
| 243 | + alt?: string |
| 244 | + width?: number |
| 245 | + height?: number |
| 246 | + fallback?: string |
| 247 | +} |
| 248 | + |
| 249 | +export function SafeImage({ src, alt, width, height, fallback = '/static/placeholder.jpg' }: ImageProps) { |
| 250 | + const safeSrc = src || fallback |
| 251 | + |
| 252 | + return ( |
| 253 | + <img |
| 254 | + src={safeSrc} |
| 255 | + alt={alt || "Image"} |
| 256 | + width={width || 300} |
| 257 | + height={height || 200} |
| 258 | + loading="lazy" |
| 259 | + onError={(e) => { |
| 260 | + const target = e.target as HTMLImageElement |
| 261 | + if (target.src !== fallback) { |
| 262 | + target.src = fallback |
| 263 | + } |
| 264 | + }} |
| 265 | + /> |
| 266 | + ) |
| 267 | +} |
| 268 | +``` |
| 269 | + |
| 270 | +## 🚀 Performance & Caching Patterns |
| 271 | + |
| 272 | +### 1. SWR Caching for Heavy Middleware |
| 273 | + |
| 274 | +**Real Implementation:** |
| 275 | +```typescript |
| 276 | +// sections/SEO/SeoPLPV2Middleware.tsx |
| 277 | +export const cache = 'stale-while-revalidate' |
| 278 | + |
| 279 | +export default function SeoPLPV2Middleware(props: Props) { |
| 280 | + // Heavy SEO computation here... |
| 281 | + return <SEOComponent {...seoData} /> |
| 282 | +} |
| 283 | +``` |
| 284 | + |
| 285 | +**Benefits:** |
| 286 | +- Reduces server load on category pages |
| 287 | +- Consistent SEO data across requests |
| 288 | +- Better performance for repeat visitors |
| 289 | + |
| 290 | +### 2. Duplicate Loader Elimination |
| 291 | + |
| 292 | +**Real Issue:** Category pages were loading product lists twice - once in breadcrumbs, once in main content |
| 293 | + |
| 294 | +**Solution:** |
| 295 | +- Remove redundant `"Lista de Produtos - 20 Itens"` from breadcrumb sections |
| 296 | +- Use shared loaders instead of duplicate API calls |
| 297 | +- Consolidate data fetching at page level |
| 298 | + |
| 299 | +## 🔧 Build System & Dependency Patterns |
| 300 | + |
| 301 | +### 1. PostCSS Peer Dependency Resolution |
| 302 | + |
| 303 | +**Real Issue Found:** |
| 304 | +``` |
| 305 | +npm WARN cssnano@6.1.2 requires a peer of postcss@^8.4.31 but postcss@8.4.27 was installed |
| 306 | +``` |
| 307 | + |
| 308 | +**Solution Pattern:** |
| 309 | +```json |
| 310 | +// deno.json |
| 311 | +{ |
| 312 | + "imports": { |
| 313 | + "postcss": "npm:postcss@8.4.38", |
| 314 | + "postcss@8.4.27": "npm:postcss@8.4.38" |
| 315 | + } |
| 316 | +} |
| 317 | +``` |
| 318 | + |
| 319 | +**Key Points:** |
| 320 | +- Pin PostCSS to satisfy cssnano peer requirements |
| 321 | +- Use alias mapping to redirect old versions |
| 322 | +- Test build after dependency changes |
| 323 | + |
| 324 | +## 📊 Testing & Validation Checklist |
| 325 | + |
| 326 | +### Error Handling Validation |
| 327 | +- [ ] Search with malformed queries (e.g., `?q=test+data:text/html`) |
| 328 | +- [ ] Components render with undefined/null image sources |
| 329 | +- [ ] API failures don't crash the entire page |
| 330 | +- [ ] Loading states are properly cleaned up on errors |
| 331 | +- [ ] Error messages include structured context for debugging |
| 332 | + |
| 333 | +### Performance Validation |
| 334 | +- [ ] No "Missing height" warnings in browser console |
| 335 | +- [ ] SWR cache headers present on middleware responses |
| 336 | +- [ ] No duplicate API calls on category pages |
| 337 | +- [ ] Image optimization working properly |
| 338 | + |
| 339 | +### Security Validation |
| 340 | +- [ ] Search parameters are properly sanitized |
| 341 | +- [ ] URL segments reject data URIs and javascript schemes |
| 342 | +- [ ] No XSS vectors through search or filter parameters |
| 343 | +- [ ] Error messages don't leak sensitive information |
| 344 | + |
| 345 | +--- |
| 346 | + |
| 347 | +**Related Documents:** |
| 348 | +- [deco-cx-store-migration-plan.md](./deco-cx-store-migration-plan.md) - Main migration guide |
| 349 | +- [deco-cx-performance-optimizations.md](./deco-cx-performance-optimizations.md) - Performance patterns |
| 350 | + |
| 351 | +**Last Updated:** September 2025 |
0 commit comments