Skip to content

Anti-Patterns & Common Mistakes

Not Using Dynamic Import

Wrong:

import { DeepChat } from 'deep-chat-react';

export default function Page() {
  return <DeepChat connect={{ url: '/api/chat' }} />;
}

Why it's bad: SSR fails with ReferenceError: customElements is not defined. DeepChat relies on browser APIs.

Correct:

import dynamic from 'next/dynamic';

const DeepChat = dynamic(
  () => import('deep-chat-react').then(mod => mod.DeepChat),
  { ssr: false }
);

Exposing API Keys Client-Side

Wrong:

<DeepChat
  directConnection={{
    openai: { key: 'sk-...' }
  }}
/>

Why it's bad: API key visible in browser. Anyone can steal it and rack up charges.

Correct:

// app/api/chat/route.ts
const response = await fetch('https://api.openai.com/v1/chat/completions', {
  headers: { 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}` },
});

Not Sanitizing HTML Responses

Wrong:

return NextResponse.json({
  html: aiResponse, // Raw AI output
});

Why it's bad: AI can be manipulated to output <script>alert('XSS')</script>. Major security vulnerability.

Correct:

import sanitizeHtml from 'sanitize-html';

const sanitized = sanitizeHtml(aiResponse, {
  allowedTags: ['p', 'b', 'i', 'a', 'ul', 'ol', 'li'],
});
return NextResponse.json({ html: sanitized });

Sending Full History to Drupal

Wrong:

{
  messages: allMessages, // 100+ messages
  thread_id: threadId,
}

Why it's bad: Drupal stores history server-side. Sending full history wastes bandwidth and is ignored by backend.

Correct:

{
  messages: [currentMessage], // Only new message
  thread_id: threadId, // Backend retrieves history
}

Not Handling Stream Errors

Wrong:

const stream = new ReadableStream({
  async start(controller) {
    const reader = response.body.getReader();
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      controller.enqueue(value);
    }
    controller.close();
  },
});

Why it's bad: If reader.read() throws, controller never closes. Client waits forever.

Correct:

const stream = new ReadableStream({
  async start(controller) {
    try {
      const reader = response.body.getReader();
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        controller.enqueue(value);
      }
    } catch (error) {
      controller.error(error);
    } finally {
      controller.close();
    }
  },
});

Caching CSRF Tokens Across Users

Wrong:

let csrfToken: string | null = null;

async function getCsrfToken() {
  if (csrfToken) return csrfToken;
  csrfToken = await fetchToken();
  return csrfToken;
}

Why it's bad: CSRF tokens are user-specific. Sharing across users causes auth failures and security issues.

Correct:

const csrfCache = new Map<string, { token: string; expires: number }>();

async function getCsrfToken(userId: string) {
  const cached = csrfCache.get(userId);
  if (cached && cached.expires > Date.now()) {
    return cached.token;
  }
  const token = await fetchToken(userId);
  csrfCache.set(userId, { token, expires: Date.now() + 1200000 });
  return token;
}

Using response.json() on Session Endpoint

Wrong:

const response = await fetch('/api/deepchat/session', { method: 'POST' });
const data = await response.json(); // Error!

Why it's bad: Drupal session endpoint returns plain text, not JSON. Parsing fails.

Correct:

const response = await fetch('/api/deepchat/session', { method: 'POST' });
const csrfToken = await response.text(); // Plain text

Not Validating File Uploads Server-Side

Wrong:

<DeepChat
  images={{
    files: { acceptedFormats: '.jpg,.png' }
  }}
/>

Why it's bad: acceptedFormats is client-side only. Users can bypass by modifying requests.

Correct:

export async function POST(request: NextRequest) {
  const formData = await request.formData();
  const files = formData.getAll('files') as File[];

  for (const file of files) {
    if (!file.type.startsWith('image/')) {
      return NextResponse.json({ error: 'Invalid type' }, { status: 400 });
    }

    // Check magic bytes
    const buffer = Buffer.from(await file.arrayBuffer());
    const isValid = buffer[0] === 0xFF && buffer[1] === 0xD8; // JPEG
    if (!isValid) {
      return NextResponse.json({ error: 'Invalid file' }, { status: 400 });
    }
  }
}

No Rate Limiting

Wrong:

export async function POST(request: NextRequest) {
  const response = await callOpenAI(); // No limits
  return NextResponse.json(response);
}

Why it's bad: Users can spam requests, costing $$$. Bots can abuse your API.

Correct:

import { Ratelimit } from '@upstash/ratelimit';

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '1 m'),
});

export async function POST(request: NextRequest) {
  const { success } = await ratelimit.limit(request.ip);
  if (!success) {
    return NextResponse.json({ error: 'Rate limit' }, { status: 429 });
  }
  // Process...
}

Not Memoizing Config Objects

Wrong:

function ChatWidget() {
  return (
    <DeepChat
      connect={{ url: '/api/chat', method: 'POST' }} // New object every render
    />
  );
}

Why it's bad: New object on every render causes DeepChat to reinitialize, breaking state.

Correct:

function ChatWidget() {
  const config = useMemo(() => ({
    url: '/api/chat',
    method: 'POST',
  }), []);

  return <DeepChat connect={config} />;
}

See Also