-
-
Notifications
You must be signed in to change notification settings - Fork 418
feat(eduaid_web): add API base fallback and backend connection status #512
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
base: main
Are you sure you want to change the base?
Changes from all commits
d81f88e
bb69539
36ef6e9
4bc43f6
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 |
|---|---|---|
| @@ -1,89 +1,179 @@ | ||
| // API client that works both in web and Electron environments | ||
| class ApiClient { | ||
| constructor() { | ||
| this.isElectron = typeof window !== 'undefined' && window.electronAPI; | ||
| this.baseUrl = this.isElectron | ||
| ? window.electronAPI.getApiConfig().baseUrl | ||
| : process.env.REACT_APP_BASE_URL || 'http://localhost:5000'; | ||
| this.isElectron = typeof window !== "undefined" && window.electronAPI; | ||
| this.baseUrl = this.isElectron | ||
| ? window.electronAPI.getApiConfig().baseUrl | ||
| : process.env.REACT_APP_BASE_URL || "http://localhost:5000"; | ||
|
|
||
| this.listeners = new Set(); | ||
| this.currentStatus = "unknown"; // unknown | up | down | error | ||
| this.currentDetail = ""; | ||
| } | ||
|
|
||
| subscribeConnectionStatus(listener) { | ||
| this.listeners.add(listener); | ||
| try { | ||
| listener({ status: this.currentStatus, detail: this.currentDetail }); | ||
| } catch (error) { | ||
| console.error("Connection listener failed:", error); | ||
| } | ||
| return () => this.listeners.delete(listener); | ||
| } | ||
BrajamohanDas-afk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| notifyConnectionStatus(status, detail = "") { | ||
| this.listeners.forEach((listener) => { | ||
| try { | ||
| listener({ status, detail }); | ||
| } catch (error) { | ||
| console.error("Connection listener failed:", error); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| setConnectionStatus(status, detail = "") { | ||
| if (this.currentStatus === status && this.currentDetail === detail) return; | ||
| this.currentStatus = status; | ||
| this.currentDetail = detail; | ||
| this.notifyConnectionStatus(status, detail); | ||
| } | ||
|
|
||
| getFallbackBaseUrls() { | ||
| if (this.isElectron || process.env.REACT_APP_BASE_URL) return []; | ||
| if (typeof window === "undefined") return []; | ||
|
|
||
| const host = window.location.hostname; | ||
| if (host !== "localhost" && host !== "127.0.0.1") return []; | ||
|
|
||
| if (this.baseUrl.includes(":5000")) return ["http://localhost:5001"]; | ||
| if (this.baseUrl.includes(":5001")) return ["http://localhost:5000"]; | ||
| return []; | ||
| } | ||
|
|
||
| async parseJsonResponse(response) { | ||
| if (!response.ok) { | ||
| const text = await response.text().catch(() => ""); | ||
| throw new Error(`HTTP ${response.status}: ${text || "Request Failed"}`); | ||
| } | ||
|
|
||
| if (response.status === 204 || response.status === 205) return null; | ||
|
|
||
| const contentLength = response.headers.get("Content-Length"); | ||
| if (contentLength === "0") return null; | ||
| return response.json(); | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| async fetchJson(url, options = {}) { | ||
| const response = await fetch(url, options); | ||
| return this.parseJsonResponse(response); | ||
| } | ||
|
|
||
| async requestWithFallback(endpoint, options = {}) { | ||
| const primaryUrl = `${this.baseUrl}${endpoint}`; | ||
|
|
||
| try { | ||
| const data = await this.fetchJson(primaryUrl, options); | ||
| this.setConnectionStatus("up"); | ||
| return data; | ||
| } catch (error) { | ||
| const isNetworkError = error instanceof TypeError; | ||
| if (!isNetworkError) { | ||
| this.setConnectionStatus("error", error.message); | ||
| throw error; | ||
| } | ||
|
|
||
| const fallbackBaseUrls = this.getFallbackBaseUrls().filter( | ||
| (url) => url !== this.baseUrl | ||
| ); | ||
|
|
||
| for (const fallbackBaseUrl of fallbackBaseUrls) { | ||
| try { | ||
| const data = await this.fetchJson(`${fallbackBaseUrl}${endpoint}`, options); | ||
| this.baseUrl = fallbackBaseUrl; | ||
| this.setConnectionStatus("up"); | ||
| return data; | ||
| } catch (fallbackError) { | ||
| if (!(fallbackError instanceof TypeError)) { | ||
| this.setConnectionStatus("error", fallbackError.message); | ||
| throw fallbackError; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| this.setConnectionStatus( | ||
| "down", | ||
| `Cannot reach backend at ${this.baseUrl}. Check server and REACT_APP_BASE_URL.` | ||
| ); | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| async makeRequest(endpoint, options = {}) { | ||
| if (this.isElectron) { | ||
| // Use Electron's IPC for API requests | ||
| try { | ||
| const response = await window.electronAPI.makeApiRequest(endpoint, options); | ||
| if (response.ok) { | ||
| return response.data; | ||
| } else { | ||
| throw new Error(`API request failed with status ${response.status}`); | ||
| if (!response.ok) { | ||
| const err = new Error(`API request failed with status ${response.status}`); | ||
| err.isHttpError = true; | ||
| this.setConnectionStatus("error", err.message); | ||
| throw err; | ||
| } | ||
| this.setConnectionStatus("up"); | ||
| return response.data; | ||
| } catch (error) { | ||
| console.error('Electron API request failed:', error); | ||
| if (error?.isHttpError) { | ||
| throw error; | ||
| } | ||
| this.setConnectionStatus("down", error?.message || "API request failed"); | ||
| throw error; | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } else { | ||
| // Use regular fetch for web environment | ||
| const url = `${this.baseUrl}${endpoint}`; | ||
| const response = await fetch(url, options); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error(`HTTP error! status: ${response.status}`); | ||
| } | ||
|
|
||
| return await response.json(); | ||
| } | ||
|
|
||
| return this.requestWithFallback(endpoint, options); | ||
| } | ||
|
|
||
| // Convenience methods for common HTTP verbs | ||
| async get(endpoint, options = {}) { | ||
| return this.makeRequest(endpoint, { ...options, method: 'GET' }); | ||
| return this.makeRequest(endpoint, { ...options, method: "GET" }); | ||
| } | ||
|
|
||
| async post(endpoint, data, options = {}) { | ||
| return this.makeRequest(endpoint, { | ||
| ...options, | ||
| method: 'POST', | ||
| method: "POST", | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| ...options.headers | ||
| "Content-Type": "application/json", | ||
| ...options.headers, | ||
| }, | ||
| body: JSON.stringify(data) | ||
| body: JSON.stringify(data), | ||
| }); | ||
| } | ||
|
|
||
| async postFormData(endpoint, formData, options = {}) { | ||
| if (this.isElectron) { | ||
| // For Electron, we need to handle file uploads differently | ||
| // Since we can't easily pass files through IPC, we'll fall back to fetch | ||
| const url = `${this.baseUrl}${endpoint}`; | ||
| const response = await fetch(url, { | ||
| ...options, | ||
| method: 'POST', | ||
| body: formData | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error(`HTTP error! status: ${response.status}`); | ||
| } | ||
|
|
||
| return await response.json(); | ||
| } else { | ||
| // For web, use FormData directly | ||
| const url = `${this.baseUrl}${endpoint}`; | ||
| const response = await fetch(url, { | ||
| ...options, | ||
| method: 'POST', | ||
| body: formData | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error(`HTTP error! status: ${response.status}`); | ||
| try { | ||
| const response = await fetch(`${this.baseUrl}${endpoint}`, { | ||
| ...options, | ||
| method: "POST", | ||
| body: formData, | ||
| }); | ||
| const data = await this.parseJsonResponse(response); | ||
| this.setConnectionStatus("up"); | ||
| return data; | ||
| } catch (error) { | ||
| const status = error instanceof TypeError ? "down" : "error"; | ||
| this.setConnectionStatus(status, error?.message || "API request failed"); | ||
| throw error; | ||
| } | ||
|
|
||
| return await response.json(); | ||
| } | ||
|
|
||
| return this.requestWithFallback(endpoint, { | ||
| ...options, | ||
| method: "POST", | ||
| body: formData, | ||
| }); | ||
| } | ||
|
Comment on lines
152
to
175
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. 🛠️ Refactor suggestion | 🟠 Major 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# First, find the apiClient.js file and understand the repository structure
find . -name "apiClient.js" -o -name "*apiClient*" -o -name "*electron*" | head -20Repository: AOSSIE-Org/EduAid Length of output: 96 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Search for makeApiRequest usage and electronAPI references
rg -n 'makeApiRequest|electronAPI|contextBridge|ipcRenderer' --type js --type ts --type jsx --type tsx -B 2 -A 2Repository: AOSSIE-Org/EduAid Length of output: 88 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Find preload scripts and main process files
fd -HI 'preload|main\.js|ipc' | grep -E '\.(js|ts)$'Repository: AOSSIE-Org/EduAid Length of output: 109 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Examine the full apiClient.js file to understand all request methods
cat -n eduaid_web/src/utils/apiClient.js | head -200Repository: AOSSIE-Org/EduAid Length of output: 6336 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Examine preload.js to see how makeApiRequest is exposed
cat -n eduaid_desktop/preload.jsRepository: AOSSIE-Org/EduAid Length of output: 2395 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Examine main.js to see the IPC handler for makeApiRequest
cat -n eduaid_desktop/main.js | head -300Repository: AOSSIE-Org/EduAid Length of output: 8621 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Search for auth, token, session, headers handling in main.js and config
rg -n 'auth|token|session|Authorization|headers' eduaid_desktop/ -A 2 -B 1Repository: AOSSIE-Org/EduAid Length of output: 1286 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Check config.js to understand the setup
find eduaid_desktop -name "config.js" -o -name "config.ts" -exec cat -n {} \;Repository: AOSSIE-Org/EduAid Length of output: 43 🏁 Script executed: #!/bin/bash
set -eulo pipefail
# Search for any FormData usage in the codebase
rg -n 'FormData|multipart' --type js --type ts -A 3 -B 1Repository: AOSSIE-Org/EduAid Length of output: 1554 Enhance IPC handler to support FormData and unify with direct fetch path.
The workaround causes inconsistencies:
Extend the IPC handler to support multipart/form-data with proper boundary generation, then route 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| // Export a singleton instance | ||
| export default new ApiClient(); | ||
| const apiClient = new ApiClient(); | ||
| export default apiClient; | ||
Uh oh!
There was an error while loading. Please reload this page.