Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
8 changes: 8 additions & 0 deletions apps/web/src/app/api/openrouter/[...path]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,14 @@ export async function POST(request: NextRequest): Promise<NextResponseType<unkno
return modelDoesNotExistResponse();
}

// Validate that messages is a non-empty array for request kinds that require it
if (
(requestBodyParsed.kind === 'chat_completions' || requestBodyParsed.kind === 'messages') &&
!Array.isArray(requestBodyParsed.body.messages)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Array-only validation still allows malformed message entries

This guard only checks that messages is an array. Requests like {"messages":[null]} or {"messages":[{"role":"user"}]} still pass here and can still throw later when downstream code reads m.role or calls content.filter(...) in the prompt-extraction helpers. Because this route is still parsing raw JSON without schema validation, it needs to reject non-object / non-conforming message items as well to fully prevent the runtime 500s this PR is targeting.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a valid concern for a follow-up, but out of scope for this PR. The Sentry error is specifically TypeError: e.messages is not iterable — meaning messages is undefined/null/non-array, not that individual items within the array are malformed. Adding full Zod schema validation for the entire request body would be a larger effort that should be done separately. The Array.isArray guard matches the existing pattern used by repairTools in the same file and the addCacheBreakpoints function in request-helpers.ts.

) {
return invalidRequestResponse();
}

const requestedModel = requestBodyParsed.body.model.trim();
const requestedModelLowerCased = requestedModel.toLowerCase();
const isLegacyOpenRouterPath = url.pathname.includes('/openrouter');
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/lib/tool-calling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export function normalizeToolCallIds(
filter: (toolCallId: string) => boolean,
maxIdLength: number | undefined
) {
if (!Array.isArray(requestToMutate.messages)) {
return;
}
for (const msg of requestToMutate.messages) {
if (msg.role === 'assistant') {
for (const toolCall of msg.tool_calls ?? []) {
Expand Down
8 changes: 5 additions & 3 deletions apps/web/src/routers/kilo-pass-router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,8 @@ describe('kiloPassRouter', () => {
const caller = await createCallerForUser(user.id);
const result = await caller.kiloPass.getState();

expect(result).toEqual({ subscription: null, isEligibleForFirstMonthPromo: true });
const promoActive = new Date() < KILO_PASS_MONTHLY_FIRST_2_MONTHS_PROMO_CUTOFF.toDate();
expect(result).toEqual({ subscription: null, isEligibleForFirstMonthPromo: promoActive });
});

it('throws BAD_REQUEST when subscription exists but user has no stripe customer', async () => {
Expand Down Expand Up @@ -732,15 +733,16 @@ describe('kiloPassRouter', () => {
});

describe('isEligibleForFirstMonthPromo in getState', () => {
it('returns isEligibleForFirstMonthPromo=true when user has no subscriptions', async () => {
it('returns isEligibleForFirstMonthPromo matching promo active state when user has no subscriptions', async () => {
const user = await insertTestUser({
google_user_email: 'kilo-pass-promo-eligible-no-sub@example.com',
});

const caller = await createCallerForUser(user.id);
const result = await caller.kiloPass.getState();

expect(result.isEligibleForFirstMonthPromo).toBe(true);
const promoActive = new Date() < KILO_PASS_MONTHLY_FIRST_2_MONTHS_PROMO_CUTOFF.toDate();
expect(result.isEligibleForFirstMonthPromo).toBe(promoActive);
expect(result.subscription).toBeNull();
});

Expand Down
Loading