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:
- Token not included:
// ❌ Wrong - missing Authorization header
fetch('/api/me')
// ✅ Correct
fetch('/api/me', {
headers: {
'Authorization': `Bearer ${token}`
}
})- 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
}- 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:
- Check email format:
// Email must be lowercase and valid format
const email = userInput.toLowerCase().trim()- Password requirements:
// Minimum 8 characters, check server requirements
if (password.length < 8) {
console.error('Password too short')
}- 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:
- 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}` }
})- 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:
- Use correct endpoint:
// ❌ Wrong - old documentation
POST /file/upload
// ✅ Correct - actual implementation
POST /api/file/{preset}/{file_name}- 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
}
})- 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:
- Check file size:
const MAX_SIZE = 10 * 1024 * 1024 // 10MB
if (file.size > MAX_SIZE) {
alert('File too large. Maximum size is 10MB.')
return
}- 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
}- 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:
- 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}`
)- 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}`)- 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:
- 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)
}- 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:
- 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
}- 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)
}- 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:
- 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}` }
})- 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:
- 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')- 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()}`)- 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:
- 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
}- 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:
- 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)}`)- 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')
}- 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:
- 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"- 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')
}- 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:
- 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
- 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- 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:
- Check server configuration:
// Development - should allow all origins
// Production - configure allowed origins in server settings
- Include credentials if needed:
fetch('/api/me', {
credentials: 'include', // Include cookies
headers: {
'Authorization': `Bearer ${token}`
}
})- 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
-
Check logs:
- Browser console for client errors
- Server logs for backend issues
- Network tab for request/response inspection
-
Provide context:
- Request ID (from
reqIdfield) - Timestamp (from
timefield) - Full error response
- Steps to reproduce
- Request ID (from
-
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
-
Verify implementation:
- Some endpoints may be stubs (accept, reject, patch action)
- Check server version and feature availability
- Consult implementation notes in endpoint documentation