Skip to content

Commit df7cdcd

Browse files
committed
fix: ML predict multipart + timeout
1 parent 7740c48 commit df7cdcd

File tree

4 files changed

+81
-38
lines changed

4 files changed

+81
-38
lines changed

backend/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def create_app(config_override=None):
4343
app,
4444
origins="*",
4545
methods=["GET", "POST", "PUT", "DELETE"],
46-
allow_headers=["Content-Type"],
46+
allow_headers=["Content-Type", "Accept", "Authorization"],
4747
)
4848

4949
# Homepage Endpoints to Delete Later

backend/services/authentication_service.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,13 @@
1414

1515
def login_user_json(data):
1616
"""Attempt to login a user"""
17-
username = data.get("username")
18-
password = data.get("password")
17+
if not data:
18+
return (
19+
jsonify({"status": "error", "message": "Invalid password or username."}),
20+
400,
21+
)
22+
username = (data.get("username") or "").strip()
23+
password = (data.get("password") or "").strip()
1924

2025
# Err on the side of caution for error messages to give no hints to
2126
# attackers

frontend/cu-bytes/app/login.tsx

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,14 @@ export default function LoginScreen() {
6767
/*
6868
Send a request to the backend endpoint to get the logged-in user's username and profile settings
6969
*/
70-
const loadSettings = async () => {
71-
70+
const loadSettings = async (loggedInAs: string) => {
7271
try {
73-
const res = await fetch(`${API_BASE_URL}/profile/retreive/${username}`);
72+
const res = await fetch(
73+
`${API_BASE_URL}/profile/retreive/${encodeURIComponent(loggedInAs)}`
74+
);
7475
const data = await res.json();
7576

76-
// Update the copy of the logged-in user's username and profile settings using the retrieved data
77-
setUsernameGlobal(username);
77+
setUsernameGlobal(loggedInAs);
7878
setHasConfiguredSettingsGlobal(data.has_configured_settings)
7979
setShowStatsGlobal(data.show_stats);
8080
setHasEggAllergyGlobal(data.has_egg_allergy);
@@ -107,35 +107,54 @@ export default function LoginScreen() {
107107
string - psswrd: The password entered by the user to login into an account
108108
*/
109109
const loginUser = async (name: string, psswrd: string) => {
110+
const trimmedUser = name.trim();
111+
const trimmedPass = psswrd.trim();
112+
setError({ message: '', status: '' });
113+
110114
try {
111115
const res = await fetch(`${API_BASE_URL}/auth/login`, {
112-
method: "POST",
113-
headers: { "Content-Type": "application/json" },
114-
body: JSON.stringify( { username: name, password: psswrd} )
115-
}
116-
);
117-
const data = await res.json();
118-
119-
// If the backend endpoint returns an error, store the error message
120-
// (The user failed to log in to an account with the entered username and password)
121-
if (data.status === 'error') {
122-
setError(data);
123-
console.log(error);
116+
method: 'POST',
117+
headers: { 'Content-Type': 'application/json' },
118+
body: JSON.stringify({
119+
username: trimmedUser,
120+
password: trimmedPass,
121+
}),
122+
});
123+
124+
let data: { status?: string; message?: string } = {};
125+
try {
126+
data = await res.json();
127+
} catch {
128+
setError({
129+
message: 'Invalid response from server.',
130+
status: 'error',
131+
});
132+
return;
124133
}
125-
// Else, update the copy of the user's username to the username they entered,
126-
// load the user's profile settings from the backend endpoint, and route the user to the 'home' page
127-
// (The user successfully logged in to an account with the entered username and password)
128-
else if (data.status === 'success') {
129-
await loadSettings();
130-
router.push("/home");
134+
135+
if (data.status === 'error' || !res.ok) {
136+
setError({
137+
message: data.message || `Login failed (${res.status})`,
138+
status: 'error',
139+
});
140+
return;
131141
}
132142

143+
if (data.status === 'success') {
144+
setUsername(trimmedUser);
145+
await loadSettings(trimmedUser);
146+
router.push('/home');
147+
}
133148
} catch (err) {
134149
console.error(err);
150+
setError({
151+
message: 'Network error. Check your connection and try again.',
152+
status: 'error',
153+
});
135154
} finally {
136155
setLoading(false);
137156
}
138-
}
157+
};
139158

140159
/*
141160
Log out the logged-in user by setting their profile settings to false, and routing to the splash page

frontend/cu-bytes/services/api.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,15 @@ console.log('Config LOCAL_IP:', LOCAL_IP);
4646

4747
const api = axios.create({
4848
baseURL: API_BASE_URL,
49-
timeout: 15000,
49+
timeout: 30000,
5050
headers: {
5151
'Content-Type': 'application/json',
5252
},
5353
});
5454

55+
/** ML upload timeout (cold start + inference on Azure can exceed 15s). */
56+
const ML_PREDICT_TIMEOUT_MS = 120000;
57+
5558
// Request interceptor
5659
api.interceptors.request.use(
5760
(config) => {
@@ -83,19 +86,20 @@ api.interceptors.response.use(
8386

8487
// API functions
8588
export const apiService = {
86-
// ML Prediction
89+
/**
90+
* POST multipart image to /ml/predict.
91+
* Uses fetch (not the JSON-configured axios instance) so the browser/RN sets
92+
* multipart boundaries; axios defaults would send application/json and break uploads.
93+
*/
8794
async predictFood(imageUri: string) {
88-
// Convert image URI to FormData for upload
8995
const formData = new FormData();
9096

91-
// For web, we need to fetch the image and convert to blob
9297
if (Platform.OS === 'web') {
9398
const response = await fetch(imageUri);
9499
const blob = await response.blob();
95100
const file = new File([blob], 'image.jpg', { type: 'image/jpeg' });
96101
formData.append('image', file);
97102
} else {
98-
// For mobile (React Native)
99103
const filename = imageUri.split('/').pop() || 'image.jpg';
100104
const match = /\.(\w+)$/.exec(filename);
101105
const type = match ? `image/${match[1]}` : 'image/jpeg';
@@ -107,12 +111,27 @@ export const apiService = {
107111
} as any);
108112
}
109113

110-
const response = await api.post('/ml/predict', formData, {
111-
headers: {
112-
'Content-Type': 'multipart/form-data',
113-
},
114-
});
115-
return response.data;
114+
const controller = new AbortController();
115+
const timeoutId = setTimeout(() => controller.abort(), ML_PREDICT_TIMEOUT_MS);
116+
117+
try {
118+
const res = await fetch(`${API_BASE_URL}/ml/predict`, {
119+
method: 'POST',
120+
body: formData,
121+
signal: controller.signal,
122+
});
123+
const data = await res.json().catch(() => ({}));
124+
if (!res.ok) {
125+
const msg =
126+
typeof data === 'object' && data && 'error' in data
127+
? String((data as { error?: string }).error)
128+
: res.statusText;
129+
throw new Error(msg || `Predict failed (${res.status})`);
130+
}
131+
return data;
132+
} finally {
133+
clearTimeout(timeoutId);
134+
}
116135
},
117136
};
118137

0 commit comments

Comments
 (0)