Skip to main content

Overview

The JOIP API uses standard HTTP status codes and returns consistent JSON error responses to help you handle errors gracefully.

Error Response Format

All error responses follow a consistent structure:
{
  "error": "Error Type",
  "message": "Human-readable error description",
  "code": "ERROR_CODE",
  "details": {}
}

Response Fields

FieldTypeDescription
errorstringError category or type
messagestringHuman-readable error message
codestringMachine-readable error code (optional)
detailsobjectAdditional context (optional)

HTTP Status Codes

Success Codes

CodeDescriptionUsage
200OKSuccessful GET, PATCH, DELETE
201CreatedSuccessful POST creating resource
302FoundRedirect after login/logout

Client Error Codes (4xx)

400 Bad Request

Invalid request data or validation errors. Common causes:
  • Missing required fields
  • Invalid data format
  • Validation failures
Example:
{
  "error": "Validation Error",
  "message": "Validation failed: title is required"
}
Example from Zod validation:
{
  "error": "Bad Request",
  "message": "Invalid input: Expected array, received string at 'subreddits'"
}

401 Unauthorized

Missing or invalid authentication. Common causes:
  • No session cookie
  • Expired session
  • Invalid credentials
Example:
{
  "message": "Unauthorized"
}
From authentication middleware (server/localAuth.ts:573-581):
export const isAuthenticated: RequestHandler = async (req, res, next) => {
  if (!req.isAuthenticated()) {
    return res.status(401).json({ message: "Unauthorized" });
  }
  if (!getUserIdOrNull(req)) {
    return res.status(401).json({ message: "Unauthorized" });
  }
  next();
};

402 Payment Required

Insufficient credits for feature access. Example (server/routes.ts:1472-1479):
{
  "error": "Insufficient credits",
  "code": "INSUFFICIENT_CREDITS",
  "required": 10,
  "current": 5,
  "featureKey": "session_create"
}

403 Forbidden

Authenticated but lacking permissions. Common causes:
  • Non-admin accessing admin endpoints
  • Accessing another user’s private resources
  • Feature restricted to premium users
Example:
{
  "error": "You do not have permission to access this session"
}
Premium-only feature (server/routes.ts:1484-1489):
{
  "error": "This feature is only available for Premium subscribers",
  "code": "PREMIUM_ONLY",
  "featureKey": "session_create"
}

404 Not Found

Requested resource doesn’t exist. Example:
{
  "message": "Session not found"
}
Unknown API routes (server/index.ts:166-168):
{
  "message": "Not Found"
}

429 Too Many Requests

Rate limit exceeded. Example (server/rateLimiter.ts:90-95):
{
  "error": "Too Many Requests",
  "message": "Rate limit exceeded. Please try again later.",
  "retryAfter": 900
}
With headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 2026-03-02T11:15:00.000Z

Server Error Codes (5xx)

500 Internal Server Error

Unexpected server error. Example (server/index.ts:187-193):
{
  "message": "Internal Server Error"
}
Common causes:
  • Database connection failures
  • Unhandled exceptions
  • External API failures

503 Service Unavailable

Service temporarily unavailable or not configured. Storage unreachable:
{
  "error": "Supabase storage is unreachable",
  "code": "STORAGE_UNREACHABLE",
  "message": "Manual sessions require storage. Please check your Supabase configuration."
}
Feature not configured:
{
  "error": "Session pricing is not configured",
  "code": "FEATURE_PRICING_MISSING",
  "featureKey": "session_create"
}

Error Codes Reference

Authentication Errors

CodeStatusDescription
UNAUTHORIZED401Missing or invalid authentication
FORBIDDEN403Insufficient permissions

Validation Errors

CodeStatusDescription
VALIDATION_ERROR400Input validation failed
INVALID_INPUT400Invalid data format
MISSING_FIELD400Required field missing

Resource Errors

CodeStatusDescription
NOT_FOUND404Resource doesn’t exist
ALREADY_EXISTS400Resource already exists
RESOURCE_DELETED404Resource has been deleted

Credit System Errors

CodeStatusDescription
INSUFFICIENT_CREDITS402Not enough credits
PREMIUM_ONLY403Feature requires premium
FEATURE_PRICING_MISSING503Feature pricing not configured

Storage Errors

CodeStatusDescription
STORAGE_CONFIG_ERROR503Storage not configured
STORAGE_UNREACHABLE503Cannot connect to storage
STORAGE_PREFLIGHT_FAILED503Storage health check failed
UPLOAD_FAILED500File upload failed

Rate Limit Errors

CodeStatusDescription
RATE_LIMIT_EXCEEDED429Too many requests

Validation Errors

Zod Validation

The API uses Zod for request validation. Validation errors include detailed information:
// From server/routes.ts:80-82
import { ZodError } from "zod";
import { fromZodError } from "zod-validation-error";
Example validation error:
{
  "error": "Bad Request",
  "message": "Validation failed",
  "details": {
    "issues": [
      {
        "code": "invalid_type",
        "expected": "string",
        "received": "number",
        "path": ["title"],
        "message": "Expected string, received number"
      }
    ]
  }
}

Input Sanitization

User inputs are sanitized before validation:
// From server/userValidation.ts
export function sanitizeUserInput(input: any): any {
  if (typeof input !== 'object' || input === null) {
    return input;
  }
  
  const sanitized: any = {};
  for (const [key, value] of Object.entries(input)) {
    if (typeof value === 'string') {
      sanitized[key] = value.trim();
    } else {
      sanitized[key] = value;
    }
  }
  return sanitized;
}

Error Handling Examples

JavaScript/TypeScript

try {
  const response = await fetch('/api/sessions', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    credentials: 'include',
    body: JSON.stringify({
      title: 'My Session',
      subreddits: ['gonewild']
    })
  });
  
  if (!response.ok) {
    const error = await response.json();
    
    switch (response.status) {
      case 401:
        // Redirect to login
        window.location.href = '/login';
        break;
        
      case 402:
        // Show credit purchase modal
        showCreditPurchaseModal(error.required - error.current);
        break;
        
      case 429:
        // Show rate limit message
        const resetTime = new Date(response.headers.get('X-RateLimit-Reset'));
        showError(`Rate limit exceeded. Try again at ${resetTime.toLocaleTimeString()}`);
        break;
        
      case 503:
        if (error.code === 'STORAGE_UNREACHABLE') {
          showError('Storage service is temporarily unavailable');
        }
        break;
        
      default:
        showError(error.message || 'An error occurred');
    }
    
    return;
  }
  
  const session = await response.json();
  console.log('Session created:', session);
  
} catch (err) {
  // Network error or JSON parse error
  console.error('Request failed:', err);
  showError('Network error. Please check your connection.');
}

Python

import requests
from datetime import datetime

try:
    response = requests.post(
        'http://localhost:5000/api/sessions',
        json={
            'title': 'My Session',
            'subreddits': ['gonewild']
        },
        cookies=session_cookies
    )
    
    response.raise_for_status()
    session = response.json()
    print(f"Session created: {session['id']}")
    
except requests.exceptions.HTTPError as e:
    error_data = e.response.json()
    
    if e.response.status_code == 401:
        print("Authentication required")
        # Redirect to login
        
    elif e.response.status_code == 402:
        print(f"Insufficient credits: need {error_data['required']}")
        # Show credit purchase option
        
    elif e.response.status_code == 429:
        retry_after = error_data.get('retryAfter', 900)
        print(f"Rate limited. Retry in {retry_after} seconds")
        
    elif e.response.status_code == 503:
        if error_data.get('code') == 'STORAGE_UNREACHABLE':
            print("Storage service unavailable")
        
    else:
        print(f"Error: {error_data.get('message', 'Unknown error')}")
        
except requests.exceptions.RequestException as e:
    print(f"Network error: {e}")

cURL with Error Handling

#!/bin/bash

response=$(curl -s -w "\n%{http_code}" \
  -X POST http://localhost:5000/api/sessions \
  -H "Content-Type: application/json" \
  -b cookies.txt \
  -d '{
    "title": "My Session",
    "subreddits": ["gonewild"]
  }')

body=$(echo "$response" | head -n -1)
status=$(echo "$response" | tail -n 1)

case $status in
  200|201)
    echo "Success: $body"
    ;;
  401)
    echo "Error: Unauthorized. Please login."
    ;;
  402)
    echo "Error: Insufficient credits."
    ;;
  429)
    echo "Error: Rate limit exceeded."
    ;;
  503)
    echo "Error: Service unavailable."
    ;;
  *)
    echo "Error ($status): $body"
    ;;
esac

Common Error Scenarios

Creating Session Without Authentication

Request:
curl -X POST http://localhost:5000/api/sessions \
  -H "Content-Type: application/json" \
  -d '{"title":"Test","subreddits":["test"]}'
Response (401):
{
  "message": "Unauthorized"
}
Solution: Include session cookie from login.

Invalid Session Data

Request:
curl -X POST http://localhost:5000/api/sessions \
  -b cookies.txt \
  -H "Content-Type: application/json" \
  -d '{"subreddits":["test"]}'
Response (400):
{
  "error": "Validation Error",
  "message": "Validation failed: title is required"
}
Solution: Include required title field.

Accessing Non-Existent Resource

Request:
curl -X GET http://localhost:5000/api/sessions/99999 \
  -b cookies.txt
Response (404):
{
  "error": "Session not found"
}
Solution: Verify session ID exists.

Rate Limit Exceeded

Request (6th login attempt in 15 minutes):
curl -X POST http://localhost:5000/api/login \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"wrong"}'
Response (429):
{
  "error": "Too Many Requests",
  "message": "Too many authentication attempts. Please try again later.",
  "retryAfter": 900
}
Solution: Wait 15 minutes or use rate limit headers to track usage.

Storage Service Down

Request:
curl -X POST http://localhost:5000/api/sessions/manual \
  -b cookies.txt \
  -F "title=Test" \
  -F "[email protected]"
Response (503):
{
  "error": "Supabase storage is unreachable",
  "code": "STORAGE_UNREACHABLE",
  "message": "Manual sessions require storage. Please check your Supabase configuration."
}
Solution: Check Supabase service status and configuration.

Error Logging

All errors are logged server-side:
// From server/index.ts:187-193
app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
  const status = err.status || err.statusCode || 500;
  const message = err.message || "Internal Server Error";

  logger.error('Server error:', err);
  res.status(status).json({ message });
});

Best Practices

1. Always Check Response Status

if (!response.ok) {
  const error = await response.json();
  throw new Error(error.message);
}

2. Handle Rate Limits Gracefully

const remaining = response.headers.get('X-RateLimit-Remaining');
if (parseInt(remaining) < 10) {
  console.warn('Approaching rate limit!');
}

3. Provide User-Friendly Messages

const friendlyMessages = {
  401: 'Please log in to continue',
  402: 'You need more credits for this action',
  403: 'You don\'t have permission to do that',
  404: 'That content could not be found',
  429: 'You\'re doing that too often. Please wait.',
  500: 'Something went wrong. Please try again.',
  503: 'This service is temporarily unavailable'
};

const message = friendlyMessages[status] || error.message;

4. Implement Retry Logic

async function fetchWithRetry(url, options, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url, options);
      
      // Don't retry client errors (4xx)
      if (response.status >= 400 && response.status < 500) {
        return response;
      }
      
      if (response.ok) {
        return response;
      }
      
      // Retry server errors (5xx)
      if (i < maxRetries - 1) {
        await new Promise(resolve => 
          setTimeout(resolve, 1000 * Math.pow(2, i))
        );
      }
    } catch (err) {
      if (i === maxRetries - 1) throw err;
    }
  }
}

5. Log Errors for Debugging

try {
  const response = await fetch(url, options);
  if (!response.ok) {
    const error = await response.json();
    console.error('API Error:', {
      url,
      status: response.status,
      error,
      timestamp: new Date().toISOString()
    });
  }
} catch (err) {
  console.error('Request failed:', err);
}

Next Steps

Build docs developers (and LLMs) love