Skip to main content

Overview

Protocol: HTTP with text/event-stream content type Communication: Unidirectional (Server → Client) Endpoint: https://api.petstoreapi.com/v1/chat/completions When to Use SSE:
  • ✅ Real-time AI chat streaming responses
  • ✅ Live progress updates
  • ✅ Server push notifications
  • ✅ One-way communication is sufficient
When NOT to Use SSE:
  • ❌ Client needs to send messages frequently (use WebSocket instead)
  • ❌ Binary data required (SSE is text-only)

How SSE Works

Client                                    Server
  │                                         │
  ├───────── POST /chat/completions ──────>│
  │         (stream: true)                  │
  │                                         │
  │<──────── data: {"delta":"Hello"} ──────┤
  │<──────── data: {"delta":" world"} ─────┤
  │<──────── data: [DONE] ─────────────────┤
  │                                         │
Key Characteristics:
  • Built on standard HTTP (no new infrastructure needed)
  • Automatic reconnection handling
  • Text-based, easy to debug
  • One-way server → client communication

Quick Start

cURL Example

curl -N -X POST https://api.petstoreapi.com/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{
    "messages": [
      {
        "role": "USER",
        "content": "Tell me about adopting Golden Retrievers"
      }
    ],
    "model": "PET_ADVISOR_1",
    "stream": true
  }'
Response (streamed):
data: {"choices":[{"delta":{"content":"Golden"},"finishReason":null}]}

data: {"choices":[{"delta":{"content":" Retrievers"},"finishReason":null}]}

data: {"choices":[{"delta":{"content":" are"},"finishReason":null}]}

data: {"choices":[{"delta":{"content":" wonderful"},"finishReason":null}]}

data: {"choices":[{"delta":{},"finishReason":"STOP"}]}

JavaScript Example

Browser (Fetch API)

const response = await fetch('https://api.petstoreapi.com/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_TOKEN'
  },
  body: JSON.stringify({
    messages: [
      {
        role: 'USER',
        content: 'What should I know before adopting a cat?'
      }
    ],
    model: 'PET_ADVISOR_1',
    stream: true
  })
});

// Process SSE stream
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullResponse = '';

while (true) {
  const { done, value } = await reader.read();
  if (done) break;

  const chunk = decoder.decode(value);
  const lines = chunk.split('\n').filter(line => line.trim());

  for (const line of lines) {
    if (line.startsWith('data: ')) {
      const data = line.slice(6);

      // Check for stream end
      if (data === '[DONE]') {
        console.log('\n--- Stream completed ---');
        console.log('Full response:', fullResponse);
        return;
      }

      try {
        const parsed = JSON.parse(data);

        // Handle streaming content
        if (parsed.choices?.[0]?.delta?.content) {
          const content = parsed.choices[0].delta.content;
          fullResponse += content;
          process.stdout.write(content); // Real-time display
        }

        // Handle completion
        if (parsed.choices?.[0]?.finish_reason === 'STOP') {
          console.log('\n--- Stream completed ---');
        }
      } catch (e) {
        console.error('Parse error:', e);
      }
    }
  }
}

Node.js (EventSource)

const EventSource = require('eventsource');

// Note: Standard EventSource only supports GET
// For POST with SSE, use fetch or a custom implementation

async function streamChatCompletion(messages) {
  const response = await fetch('https://api.petstoreapi.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${process.env.API_TOKEN}`
    },
    body: JSON.stringify({
      messages,
      model: 'PET_ADVISOR_1',
      stream: true
    })
  });

  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    const chunk = decoder.decode(value);
    for (const line of chunk.split('\n')) {
      if (line.startsWith('data: ')) {
        const data = JSON.parse(line.slice(6));
        if (data.choices?.[0]?.delta?.content) {
          console.log(data.choices[0].delta.content);
        }
      }
    }
  }
}

streamChatCompletion([
  { role: 'USER', content: 'Tell me about pet adoption' }
]);

Python Example

import requests
import json

def stream_chat_completion(messages):
    response = requests.post(
        'https://api.petstoreapi.com/v1/chat/completions',
        headers={
            'Content-Type': 'application/json',
            'Authorization': f'Bearer {api_token}'
        },
        json={
            'messages': messages,
            'model': 'PET_ADVISOR_1',
            'stream': True
        },
        stream=True
    )

    for line in response.iter_lines():
        if line:
            line = line.decode('utf-8')
            if line.startswith('data: '):
                data = line[6:]  # Remove 'data: ' prefix

                if data == '[DONE]':
                    print('\n--- Stream completed ---')
                    break

                try:
                    parsed = json.loads(data)
                    content = parsed.get('choices', [{}])[0].get('delta', {}).get('content')
                    if content:
                        print(content, end='', flush=True)
                except json.JSONDecodeError:
                    pass

# Usage
stream_chat_completion([
    {'role': 'USER', 'content': 'Tell me about adopting dogs'}
])

SSE Format

Message Format

data: {"message":"content"}

data: {"another":"message"}

data: [DONE]

Rules:
  • Each message starts with data:
  • Messages are separated by blank lines
  • End of stream is marked with data: [DONE]

Event Fields

event: messageUpdate
data: {"text":"Hello"}
id: 123
retry: 3000

data: {"text":"World"}

  • data: The actual data (required)
  • event: Event type (optional, defaults to ‘message’)
  • id: Event ID for reconnection (optional)
  • retry: Reconnection delay in milliseconds (optional)

Advanced Usage

Custom Event Types

const eventSource = new EventSource('/chat/events');

eventSource.addEventListener('message', (e) => {
  console.log('Default message:', e.data);
});

eventSource.addEventListener('typing', (e) => {
  console.log('Typing indicator:', e.data);
});

eventSource.addEventListener('error', (e) => {
  console.error('Error:', e);
});

Reconnection Handling

EventSource automatically reconnects on connection loss:
const eventSource = new EventSource('/chat/events');

eventSource.onerror = (error) => {
  console.log('Connection lost, will reconnect...');
};

// Reconnect with custom interval
const eventSource = new EventSource('/chat/events');
eventSource.onmessage = (e) => {
  const retry = parseInt(e.lastEventId);
  if (retry) {
    // Server specified custom retry interval
  }
};

Abort Controller

const controller = new AbortController();

fetch('/chat/completions', {
  signal: controller.signal
}).then(response => {
  // Process stream...
});

// Abort after 30 seconds
setTimeout(() => controller.abort(), 30000);

Comparison with Alternatives

FeatureSSEWebSocketPolling
DirectionServer → ClientBidirectionalClient → Server
OverheadLowVery LowHigh
ReconnectionAutomaticManualN/A
Browser SupportExcellentExcellentUniversal
Text Support
Binary Support
InfrastructureHTTPWebSocket ServerHTTP
Choose SSE when:
  • You only need server → client communication
  • Building on existing HTTP infrastructure
  • Implementing AI chat streaming
  • Simple reconnection handling needed
Choose WebSocket when:
  • You need bidirectional communication
  • Low latency is critical
  • Binary data transmission needed

Error Handling

try {
  const response = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ stream: true })
  });

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  const reader = response.body.getReader();

  while (true) {
    const { done, value } = await reader.read();

    if (done) {
      console.log('Stream completed normally');
      break;
    }

    // Process value...
  }

} catch (error) {
  if (error.name === 'AbortError') {
    console.log('Stream aborted by user');
  } else {
    console.error('Stream error:', error);
  }
}

Best Practices

1. Buffer Display

Don’t update the UI for every character. Buffer small amounts:
let buffer = '';
const UPDATE_INTERVAL = 50; // ms
let lastUpdate = Date.now();

stream.on('data', (chunk) => {
  buffer += chunk;

  if (Date.now() - lastUpdate > UPDATE_INTERVAL) {
    updateUI(buffer);
    buffer = '';
    lastUpdate = Date.now();
  }
});

2. Handle Stream Errors

reader.read().then(({ done, value }) => {
  if (done) return;

  try {
    const data = JSON.parse(value);
    // Process...
  } catch (e) {
    console.error('Invalid JSON in stream');
  }
}).catch(error => {
  console.error('Stream read error:', error);
});

3. Cleanup Resources

const controller = new AbortController();

try {
  await streamChat(messages, { signal: controller.signal });
} finally {
  controller.abort();
}

Troubleshooting

No Events Received

  • Check network connection
  • Verify authentication token
  • Confirm server supports streaming
  • Check browser console for errors

Connection Drops

  • SSE auto-reconnects by default
  • Implement exponential backoff if needed:
    let retryCount = 0;
    const baseDelay = 1000;
    
    eventSource.onerror = () => {
      const delay = baseDelay * Math.pow(2, retryCount);
      setTimeout(() => retryCount++, delay);
    };
    

High Memory Usage

  • Process and discard events promptly
  • Don’t accumulate entire stream in memory
  • Use streaming parsers for large payloads

Interactive Documentation