fix: reduce maxDuration to 60s for Vercel Hobby plan compatibility

Routes with maxDuration=300 were silently failing on Vercel's edge
routing layer - requests never reached the serverless function (no
logs appeared). Vercel Hobby plan has a 60s max function duration.

Affected routes:
- /api/agents/[id]/conversation/[conversationId]
- /api/agents/[username]/[uid]/conversation/[conversationId]
- /api/mcp/[username]/[slug]/[transport]

The issue caused these routes to hang indefinitely until client timeout.
This commit is contained in:
Ajax Davis 2026-01-09 07:06:54 +10:00
parent 9b6a4a625e
commit 4c5972adc4
5 changed files with 908 additions and 13 deletions

View file

@ -27,7 +27,7 @@ const CHAT_RATE_LIMIT: RateLimitConfig = {
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export const maxDuration = 300; // 5 minutes for long agentic runs
export const maxDuration = 60; // Vercel Hobby plan max
type RouteContext = {
params: Promise<{ id: string; conversationId: string }>;

View file

@ -27,7 +27,7 @@ const CHAT_RATE_LIMIT: RateLimitConfig = {
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export const maxDuration = 300; // 5 minutes for long agentic runs
export const maxDuration = 60; // Vercel Hobby plan max
type RouteContext = {
params: Promise<{ username: string; uid: string; conversationId: string }>;

View file

@ -5,7 +5,7 @@ import { handleInitialize, handleToolsCall, handleToolsList } from '~/lib/mcp/ha
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export const maxDuration = 300;
export const maxDuration = 60; // Vercel Hobby plan max
interface RouteContext {
params: Promise<{ username: string; slug: string; transport: string }>;

View file

@ -0,0 +1,903 @@
# Next.js 16 API Route Timeout Issue - Comprehensive Debug Report
**Date:** 2026-01-09
**Project:** TPMJS (tpmjs.com)
**Issue:** API routes at `/api/agents/[id]/conversation/[conversationId]` timeout without any Vercel function logs
---
## Table of Contents
1. [Problem Summary](#problem-summary)
2. [Environment Details](#environment-details)
3. [Symptoms](#symptoms)
4. [Working vs Non-Working Endpoints](#working-vs-non-working-endpoints)
5. [Directory Structure](#directory-structure)
6. [Configuration Files](#configuration-files)
7. [Route File Contents](#route-file-contents)
8. [Investigation Timeline](#investigation-timeline)
9. [Hypotheses Tested](#hypotheses-tested)
10. [Reproduction Steps](#reproduction-steps)
11. [Key Observations](#key-observations)
12. [Potential Causes](#potential-causes)
---
## Problem Summary
Requests to `/api/agents/[id]/conversation/[conversationId]` (both GET and POST) hang indefinitely and timeout after 30-60 seconds. **Critically, no logs appear in Vercel for these requests** - the requests never seem to reach the serverless function at all.
Other API routes work perfectly fine, including routes with similar structure like `/api/agents/[id]` and `/api/agents/[id]/conversations`.
---
## Environment Details
### Framework & Runtime
- **Next.js Version:** 16.0.8
- **Build System:** Turbopack (default in Next.js 16)
- **React Version:** 19.0.0
- **Node.js Runtime:** nodejs (specified in route)
- **TypeScript Version:** 5.9.3
### Deployment
- **Platform:** Vercel
- **Region:** Auto (Vercel default)
- **Monorepo:** Turborepo with pnpm
### Database
- **Database:** Neon PostgreSQL
- **ORM:** Prisma 6.19.0
- **Connection:** Working (verified via other endpoints)
### Key Dependencies (from apps/web/package.json)
```json
{
"dependencies": {
"@ai-sdk/anthropic": "^3.0.2",
"@ai-sdk/google": "^3.0.2",
"@ai-sdk/groq": "^3.0.2",
"@ai-sdk/mistral": "^3.0.2",
"@ai-sdk/openai": "3.0.1",
"@modelcontextprotocol/sdk": "^1.25.1",
"@prisma/client": "^6.19.0",
"@vercel/analytics": "^1.6.1",
"@vercel/blob": "^2.0.0",
"@vercel/kv": "^3.0.0",
"ai": "6.0.3",
"better-auth": "^1.4.10",
"next": "^16.0.8",
"prisma": "^6.19.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"zod": "^4.0.0"
}
}
```
---
## Symptoms
1. **GET/POST requests timeout** - No response after 30-60 seconds
2. **No Vercel logs** - Requests don't appear in Vercel function logs at all
3. **No errors** - No build errors, no runtime errors, nothing
4. **Route appears in build output** - Listed as dynamic function (ƒ)
5. **Similar routes work** - `/api/agents/[id]` and `/api/agents/[id]/conversations` work perfectly
---
## Working vs Non-Working Endpoints
### Working Endpoints (respond in < 1 second)
| Endpoint | Response Time | Logs in Vercel |
|----------|--------------|----------------|
| `GET /api/health` | ~0.74s | ✅ Yes |
| `GET /api/agents/cmjx48ojx0001la04lbhn98pz` | ~0.40s | ✅ Yes |
| `GET /api/agents/cmjx48ojx0001la04lbhn98pz/conversations` | ~0.50s | ✅ Yes |
| `GET /api/tools` | ~0.80s | ✅ Yes |
### Non-Working Endpoints (timeout, no logs)
| Endpoint | Result | Logs in Vercel |
|----------|--------|----------------|
| `GET /api/agents/[id]/conversation/[conversationId]` | ❌ Timeout | ❌ No logs |
| `POST /api/agents/[id]/conversation/[conversationId]` | ❌ Timeout | ❌ No logs |
| `GET /api/agents/[username]/[uid]/conversation/[conversationId]` | ❌ Timeout | ❌ No logs |
| `POST /api/agents/[username]/[uid]/conversation/[conversationId]` | ❌ Timeout | ❌ No logs |
### Pattern Identified
All routes with the path pattern `*/conversation/[conversationId]` fail. Routes with `*/conversations` (plural, no nested param) work.
---
## Directory Structure
```
apps/web/src/app/api/agents/
├── route.ts ✅ Works
├── [id]/
│ ├── route.ts ✅ Works
│ ├── like/route.ts ✅ Works
│ ├── tools/route.ts ✅ Works
│ ├── tools/[toolId]/route.ts ✅ Works (nested dynamic)
│ ├── logs/route.ts ✅ Works
│ ├── clone/route.ts ✅ Works
│ ├── conversations/route.ts ✅ Works
│ ├── collections/route.ts ✅ Works
│ ├── collections/[collectionId]/route.ts ✅ Works (nested dynamic)
│ ├── stats/route.ts ✅ Works
│ └── conversation/
│ └── [conversationId]/
│ └── route.ts ❌ FAILS - No logs
└── [username]/
└── [uid]/
└── conversation/
└── [conversationId]/
└── route.ts ❌ FAILS - No logs
```
**Key Observation:** Other nested dynamic routes work (`[id]/tools/[toolId]`, `[id]/collections/[collectionId]`), but `[id]/conversation/[conversationId]` does not.
---
## Configuration Files
### Root vercel.json
```json
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"buildCommand": "pnpm run build:web",
"outputDirectory": "apps/web/.next",
"installCommand": "pnpm install",
"framework": "nextjs",
"git": {
"deploymentEnabled": {
"main": true
}
},
"github": {
"silent": false,
"autoJobCancelation": true
},
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Strict-Transport-Security",
"value": "max-age=31536000; includeSubDomains; preload"
},
{
"key": "X-Content-Type-Options",
"value": "nosniff"
},
{
"key": "X-Frame-Options",
"value": "DENY"
},
{
"key": "X-XSS-Protection",
"value": "1; mode=block"
},
{
"key": "Referrer-Policy",
"value": "strict-origin-when-cross-origin"
}
]
}
],
"crons": [
{
"path": "/api/sync/changes",
"schedule": "0 */4 * * *"
},
{
"path": "/api/sync/keyword",
"schedule": "0 */6 * * *"
},
{
"path": "/api/sync/metrics",
"schedule": "0 0 * * *"
},
{
"path": "/api/sync/stats-snapshot",
"schedule": "0 0 * * *"
},
{
"path": "/api/sync/cleanup-activity",
"schedule": "0 3 * * *"
}
]
}
```
### apps/web/vercel.json
```json
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"buildCommand": "cd ../.. && pnpm run build:web",
"installCommand": "cd ../.. && pnpm install"
}
```
### apps/web/next.config.ts
```typescript
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
transpilePackages: ['@tpmjs/ui', '@tpmjs/utils', '@tpmjs/db', '@tpmjs/types', '@tpmjs/env'],
reactStrictMode: true,
serverExternalPackages: ['@tpmjs/package-executor'],
async redirects() {
return [
{
source: '/tools-ideas',
destination: '/tool-ideas',
permanent: true,
},
];
},
};
export default nextConfig;
```
### apps/web/src/middleware.ts
```typescript
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Protect /dashboard routes
if (request.nextUrl.pathname.startsWith('/dashboard')) {
// Check for session token - better-auth uses __Secure- prefix for HTTPS cookies
const sessionToken =
request.cookies.get('__Secure-better-auth.session_token') ||
request.cookies.get('better-auth.session_token');
if (!sessionToken) {
return NextResponse.redirect(new URL('/sign-in', request.url));
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*'],
};
```
**Note:** Middleware only matches `/dashboard/*` paths, not `/api/*` paths. Should not affect API routes.
---
## Route File Contents
### FAILING ROUTE: apps/web/src/app/api/agents/[id]/conversation/[conversationId]/route.ts
```typescript
/**
* Agent Conversation Endpoint (ID-based version)
*
* POST: Send a message and stream the AI response
* GET: Retrieve conversation history
* DELETE: Delete a conversation
*
* This endpoint uses agent id directly for dashboard usage
*/
import { decryptApiKey } from '@/lib/crypto/api-keys';
import { Prisma, prisma } from '@tpmjs/db';
import type { AIProvider } from '@tpmjs/types/agent';
import { SendMessageSchema } from '@tpmjs/types/agent';
import type { LanguageModel, ModelMessage } from 'ai';
import { type NextRequest, NextResponse } from 'next/server';
import { type RateLimitConfig, checkRateLimit } from '~/lib/rate-limit';
/**
* Rate limit for chat messages: 30 requests per minute
* This is stricter than default because chat involves expensive LLM calls
*/
const CHAT_RATE_LIMIT: RateLimitConfig = {
limit: 30,
windowSeconds: 60,
};
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export const maxDuration = 300; // 5 minutes for long agentic runs
type RouteContext = {
params: Promise<{ id: string; conversationId: string }>;
};
/**
* Get AI provider SDK based on provider type
*/
async function getProviderModel(
provider: AIProvider,
modelId: string,
apiKey: string
): Promise<LanguageModel> {
switch (provider) {
case 'OPENAI': {
const { createOpenAI } = await import('@ai-sdk/openai');
return createOpenAI({ apiKey })(modelId);
}
case 'ANTHROPIC': {
const { createAnthropic } = await import('@ai-sdk/anthropic');
return createAnthropic({ apiKey })(modelId);
}
case 'GOOGLE': {
const { createGoogleGenerativeAI } = await import('@ai-sdk/google');
return createGoogleGenerativeAI({ apiKey })(modelId);
}
case 'GROQ': {
const { createGroq } = await import('@ai-sdk/groq');
return createGroq({ apiKey })(modelId);
}
case 'MISTRAL': {
const { createMistral } = await import('@ai-sdk/mistral');
return createMistral({ apiKey })(modelId);
}
default:
throw new Error(`Unsupported provider: ${provider}`);
}
}
/**
* POST /api/agents/[id]/conversation/[conversationId]
* Send a message and stream the AI response via SSE
*/
export async function POST(request: NextRequest, context: RouteContext): Promise<Response> {
// Check rate limit first to prevent expensive LLM calls
const rateLimitResponse = checkRateLimit(request, CHAT_RATE_LIMIT);
if (rateLimitResponse) {
return rateLimitResponse;
}
const { id: agentId, conversationId } = await context.params;
try {
const body = await request.json();
const parsed = SendMessageSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ success: false, error: 'Invalid request', details: parsed.error.flatten() },
{ status: 400 }
);
}
// Fetch agent with all tool relations using agent ID
const { fetchAgentWithTools, buildAgentTools } = await import('@/lib/agents/build-tools');
const agent = await fetchAgentWithTools(agentId);
if (!agent) {
return NextResponse.json({ success: false, error: 'Agent not found' }, { status: 404 });
}
// ... rest of implementation (streaming SSE response)
} catch (error) {
console.error('Failed to process message:', error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to process message',
},
{ status: 500 }
);
}
}
/**
* GET /api/agents/[id]/conversation/[conversationId]
* Retrieve conversation history with pagination
*/
export async function GET(request: NextRequest, context: RouteContext): Promise<NextResponse> {
const { id: agentId, conversationId } = await context.params;
const { searchParams } = new URL(request.url);
const limit = Math.min(Number.parseInt(searchParams.get('limit') || '50', 10), 100);
const before = searchParams.get('before');
const after = searchParams.get('after');
try {
// Fetch agent by ID
const agent = await prisma.agent.findUnique({
where: { id: agentId },
select: { id: true },
});
if (!agent) {
return NextResponse.json({ success: false, error: 'Agent not found' }, { status: 404 });
}
// Fetch conversation
const conversation = await prisma.conversation.findUnique({
where: {
agentId_slug: {
agentId: agent.id,
slug: conversationId,
},
},
});
if (!conversation) {
return NextResponse.json(
{ success: false, error: 'Conversation not found' },
{ status: 404 }
);
}
// ... rest of implementation (simple Prisma queries)
} catch (error) {
console.error('Failed to fetch conversation:', error);
return NextResponse.json(
{ success: false, error: 'Failed to fetch conversation' },
{ status: 500 }
);
}
}
/**
* DELETE /api/agents/[id]/conversation/[conversationId]
* Delete a conversation
*/
export async function DELETE(_request: NextRequest, context: RouteContext): Promise<NextResponse> {
const { id: agentId, conversationId } = await context.params;
try {
// Fetch agent by ID
const agent = await prisma.agent.findUnique({
where: { id: agentId },
select: { id: true },
});
if (!agent) {
return NextResponse.json({ success: false, error: 'Agent not found' }, { status: 404 });
}
// Delete conversation (messages cascade)
await prisma.conversation.deleteMany({
where: {
agentId: agent.id,
slug: conversationId,
},
});
return NextResponse.json({
success: true,
data: { deleted: true },
});
} catch (error) {
console.error('Failed to delete conversation:', error);
return NextResponse.json(
{ success: false, error: 'Failed to delete conversation' },
{ status: 500 }
);
}
}
```
### WORKING ROUTE: apps/web/src/app/api/agents/[id]/conversations/route.ts (plural)
```typescript
/**
* Agent Conversations List Endpoint
*
* GET: List all conversations for an agent
*/
import { prisma } from '@tpmjs/db';
import { type NextRequest, NextResponse } from 'next/server';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export const maxDuration = 30;
type RouteContext = {
params: Promise<{ id: string }>;
};
/**
* GET /api/agents/[id]/conversations
* List all conversations for an agent (accepts id or uid)
*/
export async function GET(request: NextRequest, context: RouteContext): Promise<NextResponse> {
const { id: idOrUid } = await context.params;
const { searchParams } = new URL(request.url);
const limit = Math.min(Number.parseInt(searchParams.get('limit') || '20', 10), 100);
const offset = Number.parseInt(searchParams.get('offset') || '0', 10);
try {
// Fetch agent by id or uid
const agent = await prisma.agent.findFirst({
where: {
OR: [{ id: idOrUid }, { uid: idOrUid }],
},
select: { id: true },
});
if (!agent) {
return NextResponse.json({ success: false, error: 'Agent not found' }, { status: 404 });
}
// Fetch conversations with message count
const conversations = await prisma.conversation.findMany({
where: { agentId: agent.id },
orderBy: { updatedAt: 'desc' },
take: limit + 1,
skip: offset,
include: {
_count: {
select: { messages: true },
},
},
});
const hasMore = conversations.length > limit;
const data = hasMore ? conversations.slice(0, limit) : conversations;
return NextResponse.json({
success: true,
data: data.map((c) => ({
id: c.id,
slug: c.slug,
title: c.title,
messageCount: c._count.messages,
createdAt: c.createdAt,
updatedAt: c.updatedAt,
})),
pagination: {
limit,
offset,
hasMore,
},
});
} catch (error) {
console.error('Failed to fetch conversations:', error);
return NextResponse.json(
{ success: false, error: 'Failed to fetch conversations' },
{ status: 500 }
);
}
}
```
### WORKING ROUTE: apps/web/src/app/api/health/route.ts
```typescript
import { type NextRequest, NextResponse } from 'next/server';
import { checkRateLimit } from '~/lib/rate-limit';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export const maxDuration = 60;
/**
* GET /api/health
* Simple health check endpoint that doesn't touch the database
*/
export async function GET(request: NextRequest) {
// Check rate limit
const rateLimitResponse = checkRateLimit(request);
if (rateLimitResponse) {
return rateLimitResponse;
}
return NextResponse.json({
status: 'ok',
timestamp: new Date().toISOString(),
build: {
// Vercel provides these at runtime
commitSha: process.env.VERCEL_GIT_COMMIT_SHA?.slice(0, 7) || 'local',
commitMessage: process.env.VERCEL_GIT_COMMIT_MESSAGE || 'local',
deploymentUrl: process.env.VERCEL_URL || 'localhost',
},
env: {
hasDatabase: !!process.env.DATABASE_URL,
nodeEnv: process.env.NODE_ENV,
},
});
}
```
### WORKING ROUTE: apps/web/src/app/api/agents/[id]/route.ts
```typescript
import { Prisma, prisma } from '@tpmjs/db';
import { UpdateAgentSchema } from '@tpmjs/types/agent';
import { headers } from 'next/headers';
import type { NextRequest } from 'next/server';
import { logActivity } from '~/lib/activity';
import {
apiConflict,
apiForbidden,
apiInternalError,
apiNotFound,
apiSuccess,
apiUnauthorized,
apiValidationError,
} from '~/lib/api-response';
import { auth } from '~/lib/auth';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
type RouteContext = {
params: Promise<{ id: string }>;
};
/**
* GET /api/agents/[id]
* Get a single agent's details
*/
export async function GET(_request: NextRequest, context: RouteContext) {
const requestId = crypto.randomUUID();
try {
const session = await auth.api.getSession({ headers: await headers() });
const { id } = await context.params;
const agent = await prisma.agent.findUnique({
where: { id },
include: {
collections: {
include: {
collection: {
select: {
id: true,
name: true,
_count: { select: { tools: true } },
},
},
},
orderBy: { position: 'asc' },
take: 50,
},
tools: {
include: {
tool: {
select: {
id: true,
name: true,
description: true,
package: {
select: {
npmPackageName: true,
category: true,
},
},
},
},
},
orderBy: { position: 'asc' },
take: 100,
},
_count: {
select: {
tools: true,
collections: true,
conversations: true,
},
},
},
});
if (!agent) {
return apiNotFound('Agent', requestId);
}
// Check access - owner or public
const isOwner = session?.user?.id === agent.userId;
if (!isOwner && !agent.isPublic) {
return apiForbidden('Access denied', requestId);
}
return apiSuccess(
{
...agent,
isOwner,
toolCount: agent._count.tools,
collectionCount: agent._count.collections,
conversationCount: agent._count.conversations,
// ... mapping
},
{ requestId }
);
} catch (error) {
console.error('Failed to get agent:', error);
return apiInternalError('Failed to get agent', requestId);
}
}
// PATCH and DELETE handlers also work fine
```
---
## Investigation Timeline
### Initial State
- Routes were working previously (before refactoring)
- User refactored code, issue started
### Fix Attempt 1: Rate Limiter Timeout
- **Hypothesis:** `checkRateLimitDistributed` using `@vercel/kv` was hanging
- **Action:** Added 2-second timeout to KV calls
- **Result:** ❌ Still timing out
### Fix Attempt 2: Switch to Sync Rate Limiter
- **Hypothesis:** Async KV rate limiter had connection issues
- **Action:** Changed from `checkRateLimitDistributed` to `checkRateLimit` (sync, in-memory)
- **Result:** ❌ Still timing out
### Current State
- Route builds successfully
- Route appears in Vercel build output as dynamic function
- Requests timeout with no logs
- Other similar routes work fine
---
## Hypotheses Tested
| # | Hypothesis | Test | Result |
|---|------------|------|--------|
| 1 | Rate limiter hanging | Added timeout | ❌ Failed |
| 2 | KV connection issue | Switched to sync limiter | ❌ Failed |
| 3 | Database connection | Tested with /api/health | ✅ DB works |
| 4 | Prisma cold start | Tested other Prisma routes | ✅ Other routes work |
| 5 | Build issue | Checked build output | ✅ Route is built |
| 6 | Middleware blocking | Checked middleware config | ✅ Only matches /dashboard/* |
---
## Reproduction Steps
### 1. Test Working Endpoint
```bash
curl -s 'https://tpmjs.com/api/health' -w "\nTime: %{time_total}s"
# Returns: {"status":"ok",...} in ~0.7s
```
### 2. Test Working Agent Endpoint
```bash
curl -s 'https://tpmjs.com/api/agents/cmjx48ojx0001la04lbhn98pz' -w "\nTime: %{time_total}s"
# Returns: Agent data in ~0.4s
```
### 3. Test Working Conversations (plural) Endpoint
```bash
curl -s 'https://tpmjs.com/api/agents/cmjx48ojx0001la04lbhn98pz/conversations' -w "\nTime: %{time_total}s"
# Returns: Conversations list in ~0.5s
```
### 4. Test Failing Conversation (singular) Endpoint - GET
```bash
curl -s 'https://tpmjs.com/api/agents/cmjx48ojx0001la04lbhn98pz/conversation/conv-1767892050799-rk8eaqwhr' \
-w "\nTime: %{time_total}s" \
--max-time 60
# Result: Hangs until timeout, no response
# Vercel logs: NO ENTRY for this request
```
### 5. Test Failing Conversation Endpoint - POST
```bash
curl 'https://tpmjs.com/api/agents/cmjx48ojx0001la04lbhn98pz/conversation/conv-1767892050799-rk8eaqwhr' \
-H 'Content-Type: application/json' \
--data-raw '{"message":"hi"}' \
-w "\nTime: %{time_total}s" \
--max-time 60
# Result: Hangs until timeout, no response
# Vercel logs: NO ENTRY for this request
```
---
## Key Observations
### 1. No Vercel Logs
The most critical observation: **requests to the failing route do not appear in Vercel logs at all**. This suggests the request is not reaching the serverless function.
### 2. Route Pattern Difference
- ❌ `/api/agents/[id]/conversation/[conversationId]` - Fails
- ✅ `/api/agents/[id]/conversations` - Works
- ✅ `/api/agents/[id]/collections/[collectionId]` - Works
- ✅ `/api/agents/[id]/tools/[toolId]` - Works
The pattern `conversation/[conversationId]` fails while similar patterns like `tools/[toolId]` work.
### 3. Build Output Shows Route
```
Route (app)
├ ƒ /api/agents/[id]/conversation/[conversationId] ← Listed as dynamic function
```
### 4. Singular vs Plural
- `conversation` (singular) + nested param = ❌ Fails
- `conversations` (plural) = ✅ Works
### 5. Multiple Failing Routes
Both route variants fail:
- `/api/agents/[id]/conversation/[conversationId]/route.ts`
- `/api/agents/[username]/[uid]/conversation/[conversationId]/route.ts`
---
## Potential Causes
### 1. Next.js 16 / Turbopack Routing Bug
There may be a bug in Next.js 16 or Turbopack with:
- Specific path patterns
- Nested dynamic segments with certain names
- Routes with "conversation" in the path
### 2. Vercel Edge Routing Issue
Vercel's edge network might have issues routing to:
- Deeply nested dynamic routes
- Routes with specific naming patterns
- Routes with certain configurations
### 3. Build Artifact Corruption
The route might:
- Build successfully but not deploy correctly
- Have a corrupted manifest entry
- Be missing from the serverless function bundle
### 4. Route Conflict
There might be an internal conflict between:
- `conversation` (singular) and `conversations` (plural)
- The nested dynamic segment pattern
- Some internal Next.js routing logic
### 5. Framework Bug with Dynamic Imports
The route uses dynamic imports (`await import()`) which might cause issues with:
- Turbopack bundling
- Serverless function generation
- Module resolution
---
## Recent Commits
```
5bf4260 fix: use sync rate limiter for conversation routes
05ccdc6 fix: add timeout to Vercel KV rate limiter to prevent hanging
4db1522 fix: add ID-based conversation route for dashboard chat
7fe1027 fix: add root-level health and execute-tool routes for TPMJS compatibility
63d408b fix(executor): use correct Vercel Sandbox SDK API
```
---
## Questions for Further Investigation
1. Is this a known Next.js 16 / Turbopack issue?
2. Does the route name "conversation" conflict with any internal Next.js routing?
3. Is there a Vercel-specific issue with nested dynamic segments?
4. Why do other nested dynamic routes (`tools/[toolId]`, `collections/[collectionId]`) work?
5. Is there something specific about the combination of:
- Multiple dynamic segments (`[id]` + `[conversationId]`)
- The word "conversation"
- The route file size/complexity
---
## Files Affected
- `apps/web/src/app/api/agents/[id]/conversation/[conversationId]/route.ts`
- `apps/web/src/app/api/agents/[username]/[uid]/conversation/[conversationId]/route.ts`
## Related Working Files (for comparison)
- `apps/web/src/app/api/agents/[id]/conversations/route.ts`
- `apps/web/src/app/api/agents/[id]/tools/[toolId]/route.ts`
- `apps/web/src/app/api/agents/[id]/collections/[collectionId]/route.ts`
- `apps/web/src/app/api/agents/[id]/route.ts`
- `apps/web/src/app/api/health/route.ts`

View file

@ -88,17 +88,9 @@ export async function POST(req: NextRequest): Promise<NextResponse<ExecuteToolRe
});
}
// 2) Build environment setup for the script
const envSetup = env
? Object.entries(env)
.map(([key, value]) => `process.env[${JSON.stringify(key)}] = ${JSON.stringify(value)};`)
.join('\n')
: '';
// 3) Write the execution script (CommonJS for require())
// 2) Write the execution script (CommonJS for require())
// Note: env vars are passed via runCommand's env option, not embedded in script
const script = `
${envSetup}
(async () => {
try {
const pkg = require(${JSON.stringify(packageName)});