Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/backend/app/routers/resumes.py
Original file line number Diff line number Diff line change
Expand Up @@ -693,11 +693,11 @@ async def improve_resume_preview_endpoint(
language=language,
prompt_id=prompt_id,
),
timeout=240.0, # 4-minute hard limit
timeout=600.0, # 10-minute hard limit
)
except asyncio.TimeoutError:
logger.error(
"Improve preview timed out after 240s for resume %s / job %s",
"Improve preview timed out after 600s for resume %s / job %s",
request.resume_id,
request.job_id,
)
Expand Down
52 changes: 35 additions & 17 deletions apps/frontend/app/(default)/tailor/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,40 @@ export default function TailorPage() {
if (e.key === 'Enter') e.stopPropagation();
};

const getPreviewErrorMessage = (err: unknown) => {
const errorMessage = err instanceof Error ? err.message : String(err ?? '');
const normalized = errorMessage.toLowerCase();

if (
normalized.includes('api key') ||
normalized.includes('unauthorized') ||
normalized.includes('authentication') ||
errorMessage.includes('401')
) {
return t('tailor.errors.apiKeyError');
}

if (
normalized.includes('rate limit') ||
normalized.includes('insufficient_quota') ||
errorMessage.includes('429')
) {
return t('tailor.errors.rateLimit');
}

if (
normalized.includes('timed out') ||
normalized.includes('timeout') ||
normalized.includes('signal is aborted') ||
normalized.includes('aborterror') ||
errorMessage.includes('504')
) {
return t('tailor.errors.timeout');
}

return t('tailor.errors.failedToPreview');
};

const buildConfirmPayload = (result: ImprovedResult) => {
if (!masterResumeId) {
throw new Error('Master resume ID is missing.');
Expand Down Expand Up @@ -197,23 +231,7 @@ export default function TailorPage() {
setShowDiffModal(true);
} catch (err) {
console.error(err);
// Check for common error patterns
const errorMessage = err instanceof Error ? err.message : '';
if (
errorMessage.toLowerCase().includes('api key') ||
errorMessage.toLowerCase().includes('unauthorized') ||
errorMessage.toLowerCase().includes('authentication') ||
errorMessage.includes('401')
) {
setError(t('tailor.errors.apiKeyError'));
} else if (
errorMessage.toLowerCase().includes('rate limit') ||
errorMessage.includes('429')
) {
setError(t('tailor.errors.rateLimit'));
} else {
setError(t('tailor.errors.failedToPreview'));
}
setError(getPreviewErrorMessage(err));
}
};

Expand Down
12 changes: 9 additions & 3 deletions apps/frontend/lib/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

const DEFAULT_PUBLIC_API_URL = '/';
const INTERNAL_API_ORIGIN = 'http://127.0.0.1:8000';
const DEFAULT_REQUEST_TIMEOUT_MS = 600_000;

function normalizeApiUrl(value: string): string {
const trimmed = value.trim();
Expand Down Expand Up @@ -38,7 +39,7 @@ export const API_BASE = resolveRuntimeApiBase(toApiBase(API_URL));
*
* @param endpoint - API endpoint path or absolute URL
* @param options - Standard RequestInit options
* @param timeoutMs - Optional request timeout in milliseconds (default: 240_000)
* @param timeoutMs - Optional request timeout in milliseconds (default: 600_000)
*/
export async function apiFetch(
endpoint: string,
Expand All @@ -56,13 +57,18 @@ export async function apiFetch(
url = resolveRuntimeApiBase(normalizedEndpoint);
}

// Matches the backend's 240s hard limit (resumes.py wait_for timeout)
const timeout = timeoutMs ?? 240_000;
// Matches the backend's 10-minute hard limit for long-running AI requests.
const timeout = timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);

try {
return await fetch(url, { ...options, signal: controller.signal });
} catch (error) {
if (controller.signal.aborted) {
throw new Error(`Request timed out after ${Math.ceil(timeout / 1000)} seconds.`);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
}
throw error;
} finally {
clearTimeout(timer);
}
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/lib/api/resume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ async function postImprove(
): Promise<ImprovedResult> {
let response: Response;
try {
response = await apiPost(endpoint, payload, 240_000);
response = await apiPost(endpoint, payload, 600_000);
} catch (networkError) {
console.error(`Network error during ${endpoint}:`, networkError);
throw networkError;
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,7 @@
"jobDescriptionTooShort": "Job description is too short. Add more detail and try again.",
"apiKeyError": "Your API key isn't working. Check it in Settings.",
"rateLimit": "Rate limit reached. Wait about a minute and try again.",
"timeout": "Resume preview timed out after 10 minutes. Try again with a shorter job description or a simpler prompt.",
"failedToGenerate": "Couldn't generate the resume. Check your API key in Settings, then try again.",
"failedToPreview": "Couldn't preview the resume. Check your API key in Settings, then try again.",
"failedToConfirm": "Couldn't apply the changes. Please try again."
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,7 @@
"jobDescriptionTooShort": "La descripción del trabajo es demasiado corta. Añade más detalles e inténtalo de nuevo.",
"apiKeyError": "Tu clave API no funciona. Revísala en Configuración.",
"rateLimit": "Se alcanzó el límite de tasa. Espera aproximadamente un minuto e inténtalo de nuevo.",
"timeout": "La vista previa del currículum agotó el tiempo de espera tras 10 minutos. Inténtalo de nuevo con una descripción del trabajo más corta o un prompt más simple.",
"failedToGenerate": "No se pudo generar el currículum. Revisa tu clave API en Configuración e inténtalo de nuevo.",
"failedToPreview": "No se pudo previsualizar el currículum. Revisa tu clave API en Configuración e inténtalo de nuevo.",
"failedToConfirm": "No se pudieron aplicar los cambios. Inténtalo de nuevo."
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/messages/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,7 @@
"jobDescriptionTooShort": "求人内容が短すぎます。詳細を追加してから、もう一度お試しください。",
"apiKeyError": "API キーが機能していません。設定で確認してください。",
"rateLimit": "レート制限に達しました。約1分待ってから再試行してください。",
"timeout": "履歴書プレビューは 10 分でタイムアウトしました。求人内容を短くするか、よりシンプルなプロンプトで再試行してください。",
"failedToGenerate": "履歴書を生成できませんでした。設定で API キーを確認してから、もう一度お試しください。",
"failedToPreview": "履歴書をプレビューできませんでした。設定で API キーを確認してから、もう一度お試しください。",
"failedToConfirm": "変更を適用できませんでした。もう一度お試しください。"
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/messages/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,7 @@
"jobDescriptionTooShort": "Descrição da vaga muito curta. Adicione mais detalhes e tente novamente.",
"apiKeyError": "Sua chave de API não está funcionando. Verifique em Configurações.",
"rateLimit": "Limite de requisições atingido. Aguarde cerca de um minuto e tente novamente.",
"timeout": "A prévia do currículo expirou após 10 minutos. Tente novamente com uma descrição de vaga mais curta ou um prompt mais simples.",
"failedToGenerate": "Não foi possível gerar o currículo. Verifique sua chave de API em Configurações e tente novamente.",
"failedToPreview": "Não foi possível pré-visualizar o currículo. Verifique sua chave de API em Configurações e tente novamente.",
"failedToConfirm": "Não foi possível aplicar as alterações. Tente novamente."
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/messages/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,7 @@
"jobDescriptionTooShort": "职位描述过短。请添加更多细节后重试。",
"apiKeyError": "您的 API 密钥无效。请在设置中检查。",
"rateLimit": "已达到速率限制。请等待约一分钟后重试。",
"timeout": "简历预览在 10 分钟后超时。请缩短职位描述,或改用更简单的提示词后重试。",
"failedToGenerate": "无法生成简历。请在设置中检查 API 密钥后重试。",
"failedToPreview": "无法预览简历。请在设置中检查 API 密钥后重试。",
"failedToConfirm": "无法应用更改。请重试。"
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const BACKEND_ORIGIN = process.env.BACKEND_ORIGIN || 'http://127.0.0.1:8000';
const nextConfig: NextConfig = {
output: 'standalone',
experimental: {
proxyTimeout: 240_000,
proxyTimeout: 600_000,
// Tree-shake barrel imports — saves ~200-800ms cold start per route
optimizePackageImports: [
'lucide-react',
Expand Down