Files
OmniRoute/src/app/api/pricing/route.ts
Koosha Paridehpour 82f78b3b3b fix(api/pricing): surface validation error message as string, not raw object (#12494) (#12771)
Merged, with one adjustment.

Confirmed the bug end to end: `PricingTab.tsx:369` types the payload as `{ error?: string }` and feeds it to `new Error(errorPayload.error || ...)`, so the `{ message, details }` object landed in the toast as `[object Object]` — exactly what #12494 reported.

The one change I made before merging: `validation.error.message` is the fixed constant `"Invalid request"` (see `validateBody` in `src/shared/validation/helpers.ts:44`), so it would have swapped an unreadable toast for an uninformative one. The repo already has `formatValidationMessage()`, added in #10849 for precisely this case — it returns `"field: reason"` naming the first offending field. Merged with that instead, so a bad pricing value now says which field it was.

Validated on `release/v3.8.51`: `typecheck:core` clean, `check-file-size` OK. Rebased onto the release branch — the PR was cut from `main`, which is ~3695 commits behind the active branch.

Thank you for the report and the fix.
2026-09-05 02:33:33 -03:00

118 lines
3.5 KiB
TypeScript

import { NextResponse } from "next/server";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import {
getPricing,
getPricingWithSources,
updatePricing,
resetPricing,
resetAllPricing,
} from "@/lib/db/settings";
import { updatePricingSchema } from "@/shared/validation/schemas";
import {
formatValidationMessage,
isValidationFailure,
validateBody,
} from "@/shared/validation/helpers";
/**
* GET /api/pricing
* Get current pricing configuration (merged user + defaults)
*/
export async function GET(request: Request) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
try {
const includeSources = new URL(request.url).searchParams.get("includeSources") === "1";
if (includeSources) {
return NextResponse.json(await getPricingWithSources());
}
const pricing = await getPricing();
return NextResponse.json(pricing);
} catch (error) {
console.error("Error fetching pricing:", error);
return NextResponse.json({ error: "Failed to fetch pricing" }, { status: 500 });
}
}
/**
* PATCH /api/pricing
* Update pricing configuration
* Body: { provider: { model: { input: number, output: number, cached: number, ... } } }
*/
export async function PATCH(request) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
let rawBody;
try {
rawBody = await request.json();
} catch {
return NextResponse.json(
{
error: {
message: "Invalid request",
details: [{ field: "body", message: "Invalid JSON body" }],
},
},
{ status: 400 }
);
}
try {
const validation = validateBody(updatePricingSchema, rawBody);
if (isValidationFailure(validation)) {
// #12494: PricingTab reads this payload as `{ error?: string }` and feeds it
// straight to `new Error(...)`, so handing back the `{ message, details }`
// object rendered as "Falha ao salvar preços: [object Object]". Send a string.
// `formatValidationMessage` names the offending field ("field: reason") instead
// of the bare "Invalid request" constant, so the toast stays actionable (#10849).
return NextResponse.json(
{ error: formatValidationMessage(validation.error) },
{ status: 400 }
);
}
const body = validation.data;
const updatedPricing = await updatePricing(body);
return NextResponse.json(updatedPricing);
} catch (error) {
console.error("Error updating pricing:", error);
return NextResponse.json({ error: "Failed to update pricing" }, { status: 500 });
}
}
/**
* DELETE /api/pricing
* Reset pricing to defaults
* Query params: ?provider=xxx&model=yyy (optional)
*/
export async function DELETE(request) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
try {
const { searchParams } = new URL(request.url);
const provider = searchParams.get("provider");
const model = searchParams.get("model");
if (provider && model) {
// Reset specific model
await resetPricing(provider, model);
} else if (provider) {
// Reset entire provider
await resetPricing(provider);
} else {
// Reset all pricing
await resetAllPricing();
}
const pricing = await getPricing();
return NextResponse.json(pricing);
} catch (error) {
console.error("Error resetting pricing:", error);
return NextResponse.json({ error: "Failed to reset pricing" }, { status: 500 });
}
}