fix(preview): stop spec generation timing out behind the edge proxy
All checks were successful
Deploy to Production / deploy (push) Successful in 50s
All checks were successful
Deploy to Production / deploy (push) Successful in 50s
The /v1/servers/preview route ran claude-opus-4-7 synchronously; full spec generation routinely exceeded Cloudflare's ~100s proxy cap, so the browser received a headerless 524 and reported it as a CORS failure. - preview now uses claude-sonnet-4-6 with a 45s per-attempt timeout and one retry — comfortably inside the proxy budget - generateSpec maps an exhausted timeout to SpecTimeoutError; the route returns a clean 504 (with CORS headers) instead of a stalled connection - analyze step: live elapsed-seconds counter as freeze-proof, plus a reduced-motion exception so the loading spinner keeps spinning (a status indicator, which WCAG exempts from reduced-motion) - textarea resize grip restyled to dark theme (light hatch on dark square) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,25 +1,36 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { and, builds, buildLogs, createDb, desc, eq, mcpServers, secrets, sql, templates } from '@bmm/db';
|
||||
import { getSession } from '@bmm/auth';
|
||||
import { stopContainer } from '../lib/docker.js';
|
||||
import {
|
||||
CreateServerInput,
|
||||
IterateServerInput,
|
||||
and,
|
||||
buildLogs,
|
||||
builds,
|
||||
createDb,
|
||||
desc,
|
||||
eq,
|
||||
mcpServers,
|
||||
secrets,
|
||||
sql,
|
||||
templates,
|
||||
} from '@bmm/db';
|
||||
import { BannedPatternError, SpecTimeoutError, SpecValidationError, generateSpec } from '@bmm/llm';
|
||||
import {
|
||||
BuildEvent,
|
||||
PreviewInput,
|
||||
CreateServerInput,
|
||||
GeneratorSpec,
|
||||
IterateServerInput,
|
||||
PreviewInput,
|
||||
type SpecEdit,
|
||||
} from '@bmm/types';
|
||||
import { generateSpec, SpecValidationError, BannedPatternError } from '@bmm/llm';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { config } from '../config.js';
|
||||
import { audit } from '../lib/audit.js';
|
||||
import { encryptSecret } from '../lib/crypto.js';
|
||||
import { stopContainer } from '../lib/docker.js';
|
||||
import { cacheSpec, loadSpec, overwriteSpec } from '../lib/preview-cache.js';
|
||||
import { requireAuth } from '../plugins/session.js';
|
||||
import { getBuildQueue } from '../lib/queue.js';
|
||||
import { buildChannel, getSubscriber } from '../lib/redis.js';
|
||||
import { encryptSecret } from '../lib/crypto.js';
|
||||
import { audit } from '../lib/audit.js';
|
||||
import { requireAuth } from '../plugins/session.js';
|
||||
import { getForkRefTemplate } from './templates.js';
|
||||
import { config } from '../config.js';
|
||||
|
||||
const db = createDb();
|
||||
|
||||
@@ -42,7 +53,11 @@ export async function serverRoutes(app: FastifyInstance): Promise<void> {
|
||||
try {
|
||||
const { spec, source } = await generateSpec(parsed.data.prompt, {
|
||||
apiKey: config.ANTHROPIC_API_KEY,
|
||||
model: 'claude-opus-4-7',
|
||||
// Sonnet 4.6 drafts the spec well inside Cloudflare's ~100s proxy cap;
|
||||
// Opus routinely exceeded it, which reached the browser as a CORS error.
|
||||
model: 'claude-sonnet-4-6',
|
||||
timeoutMs: 45_000,
|
||||
maxRetries: 1,
|
||||
});
|
||||
const previewId = await cacheSpec(spec);
|
||||
return reply.send({
|
||||
@@ -67,6 +82,12 @@ export async function serverRoutes(app: FastifyInstance): Promise<void> {
|
||||
if (err instanceof BannedPatternError) {
|
||||
return reply.code(422).send({ error: 'banned_pattern', detail: err.message });
|
||||
}
|
||||
if (err instanceof SpecTimeoutError) {
|
||||
return reply.code(504).send({
|
||||
error: 'preview_timeout',
|
||||
detail: 'Spec generation took too long. Try a shorter, more specific prompt.',
|
||||
});
|
||||
}
|
||||
app.log.error(err);
|
||||
return reply.code(500).send({ error: 'preview_failed', detail: (err as Error).message });
|
||||
}
|
||||
@@ -78,7 +99,15 @@ export async function serverRoutes(app: FastifyInstance): Promise<void> {
|
||||
if (!parsed.success) {
|
||||
return reply.code(400).send({ error: 'invalid_input', issues: parsed.error.flatten() });
|
||||
}
|
||||
const { name, slug, prompt, secrets: secretValues, previewId, specEdit, templateId } = parsed.data;
|
||||
const {
|
||||
name,
|
||||
slug,
|
||||
prompt,
|
||||
secrets: secretValues,
|
||||
previewId,
|
||||
specEdit,
|
||||
templateId,
|
||||
} = parsed.data;
|
||||
|
||||
// ---- Template-fork validation ----
|
||||
// templateId is user-controlled. To prevent fork_count manipulation + garbage
|
||||
@@ -401,7 +430,10 @@ export async function serverRoutes(app: FastifyInstance): Promise<void> {
|
||||
const result = await stopContainer(server.containerId);
|
||||
containerStopped = result.ok;
|
||||
if (!result.ok) {
|
||||
app.log.warn({ containerId: server.containerId, detail: result.detail }, 'delete: stop failed');
|
||||
app.log.warn(
|
||||
{ containerId: server.containerId, detail: result.detail },
|
||||
'delete: stop failed',
|
||||
);
|
||||
}
|
||||
}
|
||||
await db.delete(mcpServers).where(eq(mcpServers.id, server.id));
|
||||
|
||||
Reference in New Issue
Block a user