Troubleshooting

API Troubleshooting Guide

Common issues and solutions when working with the Cloudillo API.

Authentication Issues

Error: E-AUTH-UNAUTH “Unauthorized access”

Cause: Missing or invalid JWT token.

Solutions:

  1. Token not included:
// ❌ Wrong - missing Authorization header
fetch('/api/me')

// ✅ Correct
fetch('/api/me', {
  headers: {
    'Authorization': `Bearer ${token}`
  }
})
  1. Token expired:
// Check token expiration
const decoded = JSON.parse(atob(token.split('.')[1]))
const isExpired = decoded.exp * 1000 < Date.now()

if (isExpired) {
  // Refresh token
  const response = await fetch('/api/auth/login-token', {
    headers: { 'Authorization': `Bearer ${oldToken}` }
  })
  const { data } = await response.json()
  token = data.token
}
  1. Token for wrong tenant:
// Ensure you're using the correct token for the target server
// Tokens from server A won't work on server B (unless using proxy tokens)

Error: E-AUTH-BADCRED “Invalid credentials”

Cause: Wrong email or password during login.

Solutions:

  1. Check email format:
// Email must be lowercase and valid format
const email = userInput.toLowerCase().trim()
  1. Password requirements:
// Minimum 8 characters, check server requirements
if (password.length < 8) {
  console.error('Password too short')
}
  1. Account not verified:
// User may need to complete email verification first
// Check for specific error code indicating verification needed
if (error.code === 'E-AUTH-NOTVERIFIED') {
  // Prompt user to check email and verify account
}

Error: E-AUTH-FORBIDDEN “Insufficient permissions”

Cause: Token doesn’t have required permission level for the operation.

Solutions:

  1. Check required permission:
// Some endpoints require 'write' permission
// POST, PATCH, DELETE typically need write or admin
// GET typically needs read

// Get access token with appropriate scope
const response = await fetch('/api/auth/access-token?scope=write', {
  headers: { 'Authorization': `Bearer ${loginToken}` }
})
  1. Use correct token type:
// Login tokens (from /api/auth/login) have full access
// Access tokens (from /api/auth/access-token) have limited scope
// Proxy tokens (from /api/auth/proxy-token) are for federation

// For most operations, use access token with required scope

File Upload Issues

Error: File upload returns 400 Bad Request

Cause: Incorrect endpoint or request format.

Solutions:

  1. Use correct endpoint:
// ❌ Wrong - old documentation
POST /file/upload

// ✅ Correct - actual implementation
POST /api/file/{preset}/{file_name}
  1. Correct multipart form:
// ❌ Wrong - JSON body
fetch('/api/file/default/image.jpg', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ file: base64Data })
})

// ✅ Correct - multipart form with file blob
const formData = new FormData()
formData.append('file', fileBlob, 'image.jpg')

fetch('/api/file/default/image.jpg', {
  method: 'POST',
  body: formData,
  headers: {
    'Authorization': `Bearer ${token}`
    // Don't set Content-Type - let browser set it with boundary
  }
})
  1. Include query parameters if needed:
const params = new URLSearchParams({
  tags: 'profile,avatar',
  created_at: Math.floor(Date.now() / 1000).toString()
})

fetch(`/api/file/profile-picture/avatar.jpg?${params}`, {
  method: 'POST',
  body: formData,
  headers: { 'Authorization': `Bearer ${token}` }
})

Error: File upload succeeds but returns 500

Cause: Image processing failure or disk space issues.

Solutions:

  1. Check file size:
const MAX_SIZE = 10 * 1024 * 1024 // 10MB

if (file.size > MAX_SIZE) {
  alert('File too large. Maximum size is 10MB.')
  return
}
  1. Validate file type:
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']

if (!ALLOWED_TYPES.includes(file.type)) {
  alert('Invalid file type. Please upload an image.')
  return
}
  1. Use simpler preset:
// If 'default' preset fails, try 'raw' (no processing)
fetch('/api/file/raw/document.pdf', {
  method: 'POST',
  body: formData,
  headers: { 'Authorization': `Bearer ${token}` }
})

WebSocket Connection Issues

WebSocket immediately disconnects

Cause: Authentication or protocol issues.

Solutions:

  1. Include token in connection:
// ❌ Wrong - no authentication
const ws = new WebSocket('wss://server.com/ws/rtdb/f1~abc123')

// ✅ Correct - token in query parameter
const ws = new WebSocket(
  `wss://server.com/ws/rtdb/f1~abc123?token=${token}`
)
  1. Use correct protocol:
// Use wss:// for HTTPS sites, ws:// for HTTP
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
const ws = new WebSocket(`${protocol}//${window.location.host}/ws/rtdb/f1~abc123?token=${token}`)
  1. Handle connection errors:
ws.onerror = (error) => {
  console.error('WebSocket error:', error)
  // Check if token is valid
  // Check if fileId exists
  // Check if user has access to file
}

ws.onclose = (event) => {
  console.log('WebSocket closed:', event.code, event.reason)
  if (event.code === 1008) {
    // Policy violation - likely auth failure
    console.error('Authentication failed')
  }
}

WebSocket connects but no messages received

Cause: Not sending required subscription or initialization messages.

Solutions:

  1. Send subscription after connection:
ws.onopen = () => {
  // Send subscription message
  ws.send(JSON.stringify({
    type: 'subscribe',
    path: '/posts'
  }))
}

ws.onmessage = (event) => {
  const message = JSON.parse(event.data)
  console.log('Received:', message)
}
  1. Check message format:
// Ensure you're parsing messages correctly
ws.onmessage = (event) => {
  try {
    const data = JSON.parse(event.data)
    handleMessage(data)
  } catch (err) {
    console.error('Failed to parse message:', event.data)
  }
}

Action API Issues

Error: POST /api/action returns 400

Cause: Invalid action type or missing required fields.

Solutions:

  1. Validate action structure:
// ❌ Wrong - missing required fields
{
  type: 'POST',
  content: 'Hello world'
}

// ✅ Correct - complete action
{
  type: 'POST',
  content: {
    text: 'Hello world'
  },
  audience: ['public'],  // Required
  published: Math.floor(Date.now() / 1000)  // Unix seconds
}
  1. Check action type:
// Valid action types (case-sensitive)
const VALID_TYPES = [
  'POST', 'COMMENT', 'REACT', 'CONNECT', 'FOLLOW',
  'CONVERSATION', 'ACKNOWLEDGE', 'FILESHARE', 'STATISTICS'
]

if (!VALID_TYPES.includes(action.type)) {
  console.error('Invalid action type:', action.type)
}
  1. Use correct field types:
// ❌ Wrong - timestamps in milliseconds
{
  published: Date.now()  // 1735000000000
}

// ✅ Correct - Unix seconds
{
  published: Math.floor(Date.now() / 1000)  // 1735000000
}

Error: Accept/Reject/Patch action endpoints return 500

Cause: These endpoints are currently stubs in the implementation.

Solutions:

  1. Check implementation status:
// ⚠️ Known limitation - these endpoints may not be fully implemented:
// - POST /api/action/:actionId/accept
// - POST /api/action/:actionId/reject
// - PATCH /api/action/:actionId

// Workaround: Use DELETE to remove unwanted actions
await fetch(`/api/action/${actionId}`, {
  method: 'DELETE',
  headers: { 'Authorization': `Bearer ${token}` }
})
  1. Alternative for connection acceptance:
// Instead of accepting a CONNECT action, create a reciprocal CONNECT
const connectBack = await fetch('/api/action', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    type: 'CONNECT',
    audience: [originalConnectIssuer],
    subject: originalConnectActionId,
    published: Math.floor(Date.now() / 1000)
  })
})

Query Parameter Issues

GET /api/action returns all actions instead of filtered results

Cause: Incorrect query parameter format or naming.

Solutions:

  1. Use correct parameter names:
// ❌ Wrong - incorrect parameter names
fetch('/api/action?actionType=POST&author=alice@example.com')

// ✅ Correct
fetch('/api/action?type=POST&issuerTag=alice@example.com')
  1. Build query string properly:
const params = new URLSearchParams({
  type: 'POST',
  status: 'A',  // Active
  issuerTag: 'alice@example.com',
  _limit: '20',
  _offset: '0'
})

fetch(`/api/action?${params.toString()}`)
  1. Check parameter values:
// Status must be single character
status: 'A'  // ✅ Active
status: 'D'  // ✅ Deleted
status: 'Active'  // ❌ Wrong

// Timestamps must be Unix seconds
createdAfter: 1735000000  // ✅
createdAfter: 1735000000000  // ❌ Wrong (milliseconds)

Pagination Issues

Results incomplete or missing

Cause: Not handling pagination correctly.

Solutions:

  1. Check hasMore flag:
async function fetchAllActions() {
  const allActions = []
  let offset = 0
  const limit = 50

  while (true) {
    const response = await fetch(
      `/api/action?_limit=${limit}&_offset=${offset}`
    )
    const { data, pagination } = await response.json()

    allActions.push(...data)

    if (!pagination.hasMore) break
    offset += limit
  }

  return allActions
}
  1. Respect limit constraints:
// Maximum _limit is typically 200
const limit = Math.min(requestedLimit, 200)

Rate Limiting

Error: 429 Too Many Requests

Cause: Exceeded rate limit.

Solutions:

  1. Check rate limit headers:
const response = await fetch('/api/action')
const remaining = response.headers.get('X-RateLimit-Remaining')
const reset = response.headers.get('X-RateLimit-Reset')

console.log(`${remaining} requests remaining until ${new Date(reset * 1000)}`)
  1. Implement exponential backoff:
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    const response = await fetch(url, options)

    if (response.status !== 429) {
      return response
    }

    // Exponential backoff: 1s, 2s, 4s
    const delay = Math.pow(2, i) * 1000
    await new Promise(resolve => setTimeout(resolve, delay))
  }

  throw new Error('Rate limit exceeded after retries')
}
  1. Batch requests:
// Instead of multiple individual requests
for (const id of actionIds) {
  await fetch(`/api/action/${id}`)  // ❌ Many requests
}

// Use filtering to get multiple items
const ids = actionIds.join(',')
await fetch(`/api/action?actionIds=${ids}`)  // ✅ Single request

Federation Issues

Actions not appearing from federated users

Cause: Federation not configured or delivery failures.

Solutions:

  1. Check DNS configuration:
# Verify DNS TXT record exists
dig TXT _cloudillo.example.com

# Should return something like:
# _cloudillo.example.com. 300 IN TXT "v=1; server=https://cloudillo.example.com"
  1. Test federation endpoint:
// Test if server is reachable
const response = await fetch('https://other-server.com/.well-known/cloudillo/id-tag?id=alice@other-server.com')

if (!response.ok) {
  console.error('Federation endpoint not reachable')
}
  1. Check inbox endpoint:
// Verify your server can receive federated actions
// POST to /api/inbox should be accessible from other servers
// Check firewall and CORS settings

Response Format Issues

Unexpected response structure

Cause: Not handling ApiResponse wrapper.

Solutions:

  1. Always unwrap data field:
// ❌ Wrong - using response directly
const response = await fetch('/api/me')
const profile = await response.json()
console.log(profile.name)  // undefined

// ✅ Correct - unwrap data field
const response = await fetch('/api/me')
const { data } = await response.json()
console.log(data.name)  // Works
  1. Handle errors properly:
const response = await fetch('/api/action')
const json = await response.json()

if (!response.ok) {
  // Error structure
  console.error(json.error.code, json.error.message)
  throw new Error(json.error.message)
}

// Success structure
const actions = json.data
  1. Use reqId for debugging:
const response = await fetch('/api/action')
const { data, reqId, time } = await response.json()

console.log(`Request ${reqId} at ${new Date(time * 1000)}`)
// Use reqId when reporting issues to support

CORS Issues

Browser blocks requests with CORS error

Cause: Cross-origin requests not allowed.

Solutions:

  1. Check server configuration:
// Development - should allow all origins
// Production - configure allowed origins in server settings
  1. Include credentials if needed:
fetch('/api/me', {
  credentials: 'include',  // Include cookies
  headers: {
    'Authorization': `Bearer ${token}`
  }
})
  1. Use proxy in development:
// In package.json (for Create React App)
{
  "proxy": "http://localhost:3000"
}

// Then use relative URLs
fetch('/api/me')  // Proxied to localhost:3000/api/me

Debugging Tips

Enable verbose logging

// Log all requests and responses
const originalFetch = window.fetch
window.fetch = async (...args) => {
  console.log('Request:', args[0], args[1])
  const response = await originalFetch(...args)
  const clone = response.clone()
  const body = await clone.json()
  console.log('Response:', response.status, body)
  return response
}

Inspect JWT tokens

function decodeJWT(token) {
  const parts = token.split('.')
  if (parts.length !== 3) {
    throw new Error('Invalid JWT format')
  }

  const payload = JSON.parse(atob(parts[1]))
  return {
    ...payload,
    expiresAt: new Date(payload.exp * 1000),
    issuedAt: new Date(payload.iat * 1000),
    isExpired: payload.exp * 1000 < Date.now()
  }
}

console.log(decodeJWT(token))

Test endpoints with curl

# Login
curl -X POST https://server.com/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"alice@example.com","password":"secret"}'

# Get profile with token
TOKEN="eyJhbGc..."
curl https://server.com/api/me \
  -H "Authorization: Bearer $TOKEN"

# Create action
curl -X POST https://server.com/api/action \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "POST",
    "content": {"text": "Hello world"},
    "audience": ["public"],
    "published": 1735000000
  }'

Network debugging

// Log all network errors
window.addEventListener('unhandledrejection', (event) => {
  if (event.reason instanceof TypeError && event.reason.message.includes('fetch')) {
    console.error('Network request failed:', event.reason)
  }
})

// Check connectivity
async function checkServerHealth() {
  try {
    const response = await fetch('/api/auth/login', { method: 'HEAD' })
    return response.ok
  } catch (err) {
    console.error('Server unreachable:', err)
    return false
  }
}

Common Error Codes

Code Meaning Common Causes
E-AUTH-UNAUTH Unauthorized Missing/invalid token
E-AUTH-BADCRED Bad credentials Wrong email/password
E-AUTH-FORBIDDEN Forbidden Insufficient permissions
E-FILE-NOTFOUND File not found Invalid fileId
E-ACTION-NOTFOUND Action not found Invalid actionId
E-PROFILE-NOTFOUND Profile not found Invalid idTag
E-VALIDATION Validation error Invalid request data
E-DUPLICATE Duplicate entry Resource already exists
E-RATELIMIT Rate limited Too many requests

Getting Help

  1. Check logs:

    • Browser console for client errors
    • Server logs for backend issues
    • Network tab for request/response inspection
  2. Provide context:

    • Request ID (from reqId field)
    • Timestamp (from time field)
    • Full error response
    • Steps to reproduce
  3. Test in isolation:

    • Use curl or Postman to eliminate client code issues
    • Try from different network to rule out connectivity
    • Test with fresh token to eliminate auth issues
  4. Verify implementation:

    • Some endpoints may be stubs (accept, reject, patch action)
    • Check server version and feature availability
    • Consult implementation notes in endpoint documentation