-
Notifications
You must be signed in to change notification settings - Fork 2
Fix user fetch network error #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
197861a
72df026
50642f6
1ed10cd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -252,18 +252,30 @@ const app = new Elysia() | |||||||||
| .use(securityHeaders) | ||||||||||
| .use( | ||||||||||
| cors({ | ||||||||||
| origin: (request) => { | ||||||||||
| // Build allowed origins list | ||||||||||
| origin: ({ request }) => { | ||||||||||
| // Get the incoming origin from the request | ||||||||||
| const incomingOrigin = request.headers.get("origin"); | ||||||||||
|
|
||||||||||
| // No origin header means same-origin request or non-browser client | ||||||||||
| if (!incomingOrigin) { | ||||||||||
| return true; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // Build allowed origins list (normalize by removing trailing slashes) | ||||||||||
| // Browser Origin headers never include trailing slashes, but env vars might | ||||||||||
| const normalizeOrigin = (origin: string) => | ||||||||||
| origin.endsWith("/") ? origin.slice(0, -1) : origin; | ||||||||||
|
|
||||||||||
| const allowedOrigins: string[] = []; | ||||||||||
|
|
||||||||||
| // Add FRONTEND_URL if configured (required in production) | ||||||||||
| if (env.FRONTEND_URL) { | ||||||||||
| allowedOrigins.push(env.FRONTEND_URL); | ||||||||||
| allowedOrigins.push(normalizeOrigin(env.FRONTEND_URL)); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // Add any additional CORS_ALLOWED_ORIGINS | ||||||||||
| if (env.CORS_ALLOWED_ORIGINS && env.CORS_ALLOWED_ORIGINS.length > 0) { | ||||||||||
| allowedOrigins.push(...env.CORS_ALLOWED_ORIGINS); | ||||||||||
| allowedOrigins.push(...env.CORS_ALLOWED_ORIGINS.map(normalizeOrigin)); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // In development, allow localhost origins | ||||||||||
|
|
@@ -276,16 +288,30 @@ const app = new Elysia() | |||||||||
| ); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // If no origins configured in production, reject (don't fall back to wildcard) | ||||||||||
| // If no origins configured in production, reject | ||||||||||
| if (allowedOrigins.length === 0) { | ||||||||||
| logger.warn( | ||||||||||
| { context: "cors" }, | ||||||||||
| "No CORS origins configured - rejecting cross-origin requests", | ||||||||||
| { context: "cors", origin: incomingOrigin }, | ||||||||||
| "No CORS origins configured - rejecting cross-origin request", | ||||||||||
| ); | ||||||||||
| return false; | ||||||||||
| } | ||||||||||
|
Comment on lines
292
to
298
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Error Handling/UX Issue: Recommended Solution: |
||||||||||
|
|
||||||||||
| return allowedOrigins; | ||||||||||
| // Check if incoming origin is in allowed list | ||||||||||
| const isAllowed = allowedOrigins.includes(incomingOrigin); | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current check To make this check more robust, you should account for the potential trailing slash on the allowed origins during comparison. const isAllowed = allowedOrigins.some(allowedOrigin => allowedOrigin === incomingOrigin || (allowedOrigin.endsWith('/') && allowedOrigin.slice(0, -1) === incomingOrigin));
Comment on lines
+300
to
+301
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: Prevent potential null value errors
Suggested change
|
||||||||||
|
|
||||||||||
| if (!isAllowed) { | ||||||||||
| logger.warn( | ||||||||||
| { | ||||||||||
| context: "cors", | ||||||||||
| origin: incomingOrigin, | ||||||||||
| allowed: allowedOrigins, | ||||||||||
| }, | ||||||||||
| "CORS request from unauthorized origin rejected", | ||||||||||
| ); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| return isAllowed; | ||||||||||
| }, | ||||||||||
| credentials: true, | ||||||||||
| methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"], | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,9 @@ | ||
| import { retryWithBackoff, RetryOptions } from './retry' | ||
| import { getAuthToken } from './auth-token-store' | ||
| import { retryWithBackoff, RetryOptions } from "./retry"; | ||
| import { getAuthToken } from "./auth-token-store"; | ||
|
|
||
| export interface RequestOptions extends RequestInit { | ||
| timeoutMs?: number | ||
| retry?: RetryOptions | boolean | ||
| timeoutMs?: number; | ||
| retry?: RetryOptions | boolean; | ||
| } | ||
|
|
||
| // Get API base URL for constructing full URLs | ||
|
|
@@ -16,79 +16,92 @@ const getApiBaseUrl = (): string => { | |
| } | ||
|
|
||
| // In production, use VITE_API_URL if set, otherwise relative URLs | ||
| return import.meta.env.VITE_API_URL || ""; | ||
| } | ||
| // Remove trailing slash to prevent double-slash URLs (e.g., //api/users/me) | ||
| const baseUrl = import.meta.env.VITE_API_URL || ""; | ||
| return baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl; | ||
| }; | ||
|
|
||
| export async function apiFetch( | ||
| input: string, | ||
| init: RequestOptions = {}, | ||
| ): Promise<Response> { | ||
|
Comment on lines
+24
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Find api-client.ts file
echo "=== Searching for api-client.ts ==="
find . -name "api-client.ts" -type f 2>/dev/null | head -20
# Find the specific file under review
echo -e "\n=== Searching for the api.ts file under review ==="
find . -path "*/core/src/utils/api.ts" -type f 2>/dev/nullRepository: Dexploarer/hyper-forge Length of output: 216 🏁 Script executed: #!/bin/bash
# Read the api-client.ts file to check for apiFetch export
echo "=== Content of api-client.ts ==="
cat -n ./apps/core/src/lib/api-client.ts
echo -e "\n=== Content of api.ts (first 50 lines) ==="
head -50 ./apps/core/src/utils/api.ts
# Search for imports of apiFetch across the codebase
echo -e "\n=== Imports of apiFetch in codebase ==="
rg -n "import.*apiFetch|from.*api" --type=ts --type=tsx -A1 -B1 | head -100Repository: Dexploarer/hyper-forge Length of output: 11868 🏁 Script executed: #!/bin/bash
# Search for imports of apiFetch - fix the file type filter
echo "=== Imports of apiFetch in codebase ==="
rg -n "import.*apiFetch" --type=ts -B2 -A2
echo -e "\n=== Files that import from api-client ==="
rg -n "from.*['\"].*api-client" --type=ts
echo -e "\n=== Files that import from utils/api ==="
rg -n "from.*['\"].*utils/api" --type=tsRepository: Dexploarer/hyper-forge Length of output: 11685 Two Verified: 🤖 Prompt for AI Agents |
||
| const { timeoutMs = 15000, signal, retry: retryConfig, ...rest } = init; | ||
|
|
||
| export async function apiFetch(input: string, init: RequestOptions = {}): Promise<Response> { | ||
| const { timeoutMs = 15000, signal, retry: retryConfig, ...rest } = init | ||
|
|
||
| // Construct full URL if input is a relative path | ||
| // If input is already absolute (http:// or https://), use it as-is | ||
| // Otherwise, prepend base URL (empty string in dev/prod means relative URL) | ||
| const url = input.startsWith('http://') || input.startsWith('https://') | ||
| ? input | ||
| : `${getApiBaseUrl()}${input.startsWith('/') ? input : `/${input}`}` | ||
|
|
||
| const baseUrl = getApiBaseUrl(); | ||
| const url = | ||
| input.startsWith("http://") || input.startsWith("https://") | ||
| ? input | ||
| : baseUrl && !input.startsWith("/") | ||
| ? `${baseUrl}/${input}` | ||
| : `${baseUrl}${input}`; | ||
|
|
||
| const fetchWithTimeout = async (): Promise<Response> => { | ||
| const controller = new AbortController() | ||
| const timeout = setTimeout(() => controller.abort(new DOMException('Timeout', 'AbortError')), timeoutMs) | ||
| const controller = new AbortController(); | ||
| const timeout = setTimeout( | ||
| () => controller.abort(new DOMException("Timeout", "AbortError")), | ||
| timeoutMs, | ||
| ); | ||
|
|
||
| try { | ||
| // Get auth token and add to headers | ||
| // Only set Authorization from global token if not already explicitly provided | ||
| const token = getAuthToken() | ||
| const token = getAuthToken(); | ||
|
|
||
| // Convert rest.headers to a plain object, handling Headers objects, arrays, and plain objects | ||
| const headers: Record<string, string> = {} | ||
| const headers: Record<string, string> = {}; | ||
|
|
||
| if (rest.headers) { | ||
| if (rest.headers instanceof Headers) { | ||
| // Headers object - iterate over entries | ||
| rest.headers.forEach((value, key) => { | ||
| headers[key] = value | ||
| }) | ||
| headers[key] = value; | ||
| }); | ||
| } else if (Array.isArray(rest.headers)) { | ||
| // Array of [key, value] tuples | ||
| rest.headers.forEach(([key, value]) => { | ||
| headers[key] = value | ||
| }) | ||
| headers[key] = value; | ||
| }); | ||
| } else { | ||
| // Plain object | ||
| Object.assign(headers, rest.headers) | ||
| Object.assign(headers, rest.headers); | ||
| } | ||
| } | ||
|
Comment on lines
+54
to
71
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Header Key Case Sensitivity and Overwrite RiskThe header merging logic does not normalize header keys to a consistent case, which can result in duplicate headers (e.g., both 'Authorization' and 'authorization') or missed overwrites. This can lead to security issues if the intended header is not set or is overridden unexpectedly. Recommended Solution: const normalizedHeaders = Object.fromEntries(
Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v])
);Then check for 'authorization' only. |
||
|
|
||
| // Only set Authorization header from global token if it's not already set | ||
| // This allows explicit Authorization headers (e.g., from accessToken parameter) to take precedence | ||
| // Check both 'Authorization' and 'authorization' for case-insensitivity | ||
| const hasAuthHeader = headers['Authorization'] || headers['authorization'] | ||
| const hasAuthHeader = | ||
| headers["Authorization"] || headers["authorization"]; | ||
| if (token && !hasAuthHeader) { | ||
| headers['Authorization'] = `Bearer ${token}` | ||
| headers["Authorization"] = `Bearer ${token}`; | ||
| } | ||
|
|
||
| const response = await fetch(url, { | ||
| ...rest, | ||
| headers, | ||
| signal: signal ?? controller.signal | ||
| }) | ||
| return response | ||
| signal: signal ?? controller.signal, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential Timeout Bypass Due to Signal PrecedenceThe code uses Recommended Solution: if (signal) {
signal.addEventListener('abort', () => controller.abort(signal.reason), { once: true });
}Then always pass |
||
| }); | ||
| return response; | ||
| } finally { | ||
| clearTimeout(timeout) | ||
| clearTimeout(timeout); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| // Apply retry logic if enabled | ||
| if (retryConfig) { | ||
| const retryOptions = retryConfig === true ? {} : retryConfig | ||
| const result = await retryWithBackoff(fetchWithTimeout, retryOptions) | ||
| const retryOptions = retryConfig === true ? {} : retryConfig; | ||
| const result = await retryWithBackoff(fetchWithTimeout, retryOptions); | ||
|
|
||
| if (result.success && result.data) { | ||
| return result.data | ||
| return result.data; | ||
| } | ||
| throw result.error || new Error('Request failed after retries') | ||
|
|
||
| throw result.error || new Error("Request failed after retries"); | ||
| } | ||
|
|
||
| // No retry - direct fetch | ||
| return fetchWithTimeout() | ||
| } | ||
| return fetchWithTimeout(); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: The
@elysiajs/corsorigincallback incorrectly destructures({ request })instead of receiving the fullContextobject.Severity: CRITICAL | Confidence: High
🔍 Detailed Analysis
The
origincallback for@elysiajs/corsis incorrectly destructuring({ request })from its parameter. The callback expects a fullContextobject, not an object with a top-levelrequestproperty. This causesrequestto beundefined, leading to aTypeError: Cannot read property 'headers' of undefinedwhenrequest.headers.get("origin")is called, which will crash the server when processing CORS requests.💡 Suggested Fix
Change the
origincallback signature from({ request }) => { ... }to(context) => { ... }and access the request viacontext.request.🤖 Prompt for AI Agent
Did we get this right? 👍 / 👎 to inform future reviews.
Reference ID:
3979100