Skip to content

Commit 282194a

Browse files
committed
Fixes
1 parent bb53e06 commit 282194a

File tree

3 files changed

+121
-23
lines changed

3 files changed

+121
-23
lines changed

web/src/app/api/v1/freebuff/session/_handlers.ts

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,38 @@ async function resolveUser(req: NextRequest, deps: FreebuffSessionDeps): Promise
4949
return { userId: String(userInfo.id) }
5050
}
5151

52+
function serverError(
53+
deps: FreebuffSessionDeps,
54+
route: string,
55+
userId: string | null,
56+
error: unknown,
57+
): NextResponse {
58+
const err = error instanceof Error ? error : new Error(String(error))
59+
deps.logger.error(
60+
{
61+
route,
62+
userId,
63+
errorName: err.name,
64+
errorMessage: err.message,
65+
errorCode: (err as any).code,
66+
cause:
67+
(err as any).cause instanceof Error
68+
? {
69+
name: (err as any).cause.name,
70+
message: (err as any).cause.message,
71+
code: (err as any).cause.code,
72+
}
73+
: (err as any).cause,
74+
stack: err.stack,
75+
},
76+
'[freebuff/session] handler failed',
77+
)
78+
return NextResponse.json(
79+
{ error: 'internal_error', message: err.message },
80+
{ status: 500 },
81+
)
82+
}
83+
5284
/** POST /api/v1/freebuff/session — join queue / take over as this instance. */
5385
export async function postFreebuffSession(
5486
req: NextRequest,
@@ -57,11 +89,15 @@ export async function postFreebuffSession(
5789
const auth = await resolveUser(req, deps)
5890
if ('error' in auth) return auth.error
5991

60-
const state = await requestSession({
61-
userId: auth.userId,
62-
deps: deps.sessionDeps,
63-
})
64-
return NextResponse.json(state, { status: 200 })
92+
try {
93+
const state = await requestSession({
94+
userId: auth.userId,
95+
deps: deps.sessionDeps,
96+
})
97+
return NextResponse.json(state, { status: 200 })
98+
} catch (error) {
99+
return serverError(deps, 'POST', auth.userId, error)
100+
}
65101
}
66102

67103
/** GET /api/v1/freebuff/session — read current state without mutation. */
@@ -72,17 +108,21 @@ export async function getFreebuffSession(
72108
const auth = await resolveUser(req, deps)
73109
if ('error' in auth) return auth.error
74110

75-
const state = await getSessionState({
76-
userId: auth.userId,
77-
deps: deps.sessionDeps,
78-
})
79-
if (!state) {
80-
return NextResponse.json(
81-
{ status: 'none', message: 'Call POST to join the waiting room.' },
82-
{ status: 200 },
83-
)
111+
try {
112+
const state = await getSessionState({
113+
userId: auth.userId,
114+
deps: deps.sessionDeps,
115+
})
116+
if (!state) {
117+
return NextResponse.json(
118+
{ status: 'none', message: 'Call POST to join the waiting room.' },
119+
{ status: 200 },
120+
)
121+
}
122+
return NextResponse.json(state, { status: 200 })
123+
} catch (error) {
124+
return serverError(deps, 'GET', auth.userId, error)
84125
}
85-
return NextResponse.json(state, { status: 200 })
86126
}
87127

88128
/** DELETE /api/v1/freebuff/session — end session / leave queue immediately. */
@@ -93,6 +133,10 @@ export async function deleteFreebuffSession(
93133
const auth = await resolveUser(req, deps)
94134
if ('error' in auth) return auth.error
95135

96-
await endUserSession({ userId: auth.userId, deps: deps.sessionDeps })
97-
return NextResponse.json({ status: 'ended' }, { status: 200 })
136+
try {
137+
await endUserSession({ userId: auth.userId, deps: deps.sessionDeps })
138+
return NextResponse.json({ status: 'ended' }, { status: 200 })
139+
} catch (error) {
140+
return serverError(deps, 'DELETE', auth.userId, error)
141+
}
98142
}

web/src/server/fireworks-monitor/monitor.ts

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,46 @@ function jittered(intervalMs: number): number {
108108
return Math.max(1_000, Math.round(intervalMs + delta))
109109
}
110110

111+
/** Unwrap nested `.cause` chains (undici's `fetch failed` wraps the real
112+
* error — DNS, ECONNREFUSED, TLS, etc. — under `.cause`). */
113+
function describeError(error: unknown): {
114+
message: string
115+
name?: string
116+
code?: string
117+
causes: Array<{ name?: string; message: string; code?: string }>
118+
stack?: string
119+
} {
120+
const causes: Array<{ name?: string; message: string; code?: string }> = []
121+
let cursor: unknown = error instanceof Error ? (error as any).cause : undefined
122+
let guard = 0
123+
while (cursor && guard < 5) {
124+
if (cursor instanceof Error) {
125+
causes.push({
126+
name: cursor.name,
127+
message: cursor.message,
128+
code: (cursor as any).code,
129+
})
130+
cursor = (cursor as any).cause
131+
} else {
132+
causes.push({ message: String(cursor) })
133+
break
134+
}
135+
guard++
136+
}
137+
return {
138+
message: error instanceof Error ? error.message : String(error),
139+
name: error instanceof Error ? error.name : undefined,
140+
code: error instanceof Error ? (error as any).code : undefined,
141+
causes,
142+
stack: error instanceof Error ? error.stack : undefined,
143+
}
144+
}
145+
111146
async function pollOnce(): Promise<void> {
112147
if (!state) return
113148
const controller = new AbortController()
114149
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
150+
const url = FIREWORKS_METRICS_URL(state.options.accountId)
115151
try {
116152
const metrics = await scrapeFireworksMetrics({
117153
apiKey: state.options.apiKey,
@@ -123,8 +159,8 @@ async function pollOnce(): Promise<void> {
123159
state.lastError = null
124160
state.backoffUntil = 0
125161
} catch (error) {
126-
const message = error instanceof Error ? error.message : String(error)
127-
state.lastError = message
162+
const details = describeError(error)
163+
state.lastError = details.message
128164
if (error instanceof FireworksScrapeError && error.status === 429) {
129165
const backoffMs = error.retryAfterMs ?? DEFAULT_429_BACKOFF_MS
130166
state.backoffUntil = Date.now() + backoffMs
@@ -133,7 +169,20 @@ async function pollOnce(): Promise<void> {
133169
'[FireworksMonitor] Rate limited, backing off',
134170
)
135171
} else {
136-
logger.warn({ error: message }, '[FireworksMonitor] Scrape failed')
172+
logger.warn(
173+
{
174+
error: details.message,
175+
errorName: details.name,
176+
errorCode: details.code,
177+
causes: details.causes,
178+
aborted: controller.signal.aborted,
179+
url,
180+
accountId: state.options.accountId,
181+
usingCustomFetch: Boolean(state.options.fetch),
182+
stack: details.stack,
183+
},
184+
'[FireworksMonitor] Scrape failed',
185+
)
137186
}
138187
} finally {
139188
clearTimeout(timeout)

web/src/server/free-session/store.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ export async function joinOrTakeOver(params: {
5252
const { userId, now } = params
5353
const nextInstanceId = newInstanceId()
5454

55+
// postgres-js does NOT coerce raw JS Date values when they're interpolated
56+
// inside a `sql\`...\`` fragment (the column-type hint that Drizzle's
57+
// values() path relies on is absent there). Pre-serialize to an ISO string
58+
// and cast to timestamptz so the driver binds it as text.
59+
const nowIso = sql`${now.toISOString()}::timestamptz`
5560
// Single UPSERT that encodes every case in one round-trip, race-safe
5661
// against concurrent POSTs for the same user (the PK would otherwise turn
5762
// two parallel INSERTs into a 500). Inside ON CONFLICT DO UPDATE, bare
@@ -63,7 +68,7 @@ export async function joinOrTakeOver(params: {
6368
// queued → rotate instance_id, preserve queued_at
6469
// active & expired → re-queue at back: status=queued,
6570
// queued_at=now, admitted_at/expires_at=null
66-
const activeUnexpired = sql`${schema.freeSession.status} = 'active' AND ${schema.freeSession.expires_at} > ${now}`
71+
const activeUnexpired = sql`${schema.freeSession.status} = 'active' AND ${schema.freeSession.expires_at} > ${nowIso}`
6772

6873
const [row] = await db
6974
.insert(schema.freeSession)
@@ -84,7 +89,7 @@ export async function joinOrTakeOver(params: {
8489
queued_at: sql`CASE
8590
WHEN ${schema.freeSession.status} = 'queued' THEN ${schema.freeSession.queued_at}
8691
WHEN ${activeUnexpired} THEN ${schema.freeSession.queued_at}
87-
ELSE ${now}
92+
ELSE ${nowIso}
8893
END`,
8994
admitted_at: sql`CASE WHEN ${activeUnexpired} THEN ${schema.freeSession.admitted_at} ELSE NULL END`,
9095
expires_at: sql`CASE WHEN ${activeUnexpired} THEN ${schema.freeSession.expires_at} ELSE NULL END`,
@@ -152,7 +157,7 @@ export async function queuePositionFor(params: {
152157
.where(
153158
and(
154159
eq(schema.freeSession.status, 'queued'),
155-
sql`(${schema.freeSession.queued_at}, ${schema.freeSession.user_id}) <= (${params.queuedAt}, ${params.userId})`,
160+
sql`(${schema.freeSession.queued_at}, ${schema.freeSession.user_id}) <= (${params.queuedAt.toISOString()}::timestamptz, ${params.userId})`,
156161
),
157162
)
158163
return Number(rows[0]?.n ?? 0)

0 commit comments

Comments
 (0)