OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / lib / response.js

160 lines · javascript ref:
/**
 * Response Helpers - Standardized HTTP response utilities
 *
 * Reduces boilerplate across route handlers
 */

/**
 * Send JSON response
 */
export function json(res, data, status = 200) {
  res.statusCode = status
  res.setHeader('Content-Type', 'application/json')
  res.setHeader('X-Content-Type-Options', 'nosniff')
  res.end(JSON.stringify(data))
}

/**
 * Send HTML response with security headers
 */
export function html(res, content, status = 200) {
  res.statusCode = status
  res.setHeader('Content-Type', 'text/html')
  res.setHeader('X-Frame-Options', 'DENY')
  res.setHeader('X-Content-Type-Options', 'nosniff')
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin')
  res.end(content)
}

/**
 * Send plain text response
 */
export function text(res, content, status = 200) {
  res.statusCode = status
  res.setHeader('Content-Type', 'text/plain')
  res.end(content)
}

/**
 * Send error JSON response
 */
export function error(res, message, status = 500) {
  res.statusCode = status
  res.setHeader('Content-Type', 'application/json')
  res.end(JSON.stringify({ error: message }))
}

/**
 * Send 404 Not Found response
 */
export function notFound(res, message = 'Not found') {
  error(res, message, 404)
}

/**
 * Send 403 Forbidden response
 */
export function forbidden(res, message = 'Forbidden') {
  error(res, message, 403)
}

/**
 * Send 401 Unauthorized response with WWW-Authenticate header
 */
export function unauthorized(res, message = 'Unauthorized') {
  res.statusCode = 401
  res.setHeader('WWW-Authenticate', 'Bearer realm="Pear Git"')
  res.setHeader('Content-Type', 'application/json')
  res.end(JSON.stringify({ error: message }))
}

/**
 * Send 400 Bad Request response
 */
export function badRequest(res, message = 'Bad request') {
  error(res, message, 400)
}

/**
 * Send 429 Rate Limit response
 */
export function rateLimited(res, retryAfter, resetAt) {
  res.statusCode = 429
  res.setHeader('Retry-After', String(retryAfter))
  res.setHeader('X-RateLimit-Reset', String(resetAt))
  res.setHeader('Content-Type', 'application/json')
  res.end(JSON.stringify({
    error: 'Rate limit exceeded',
    retryAfter,
    resetAt,
    message: `Too many requests. Try again in ${retryAfter} seconds.`
  }))
}

/**
 * Send NDJSON streaming response (start)
 * Returns a writer function for sending chunks
 */
export function ndjsonStream(res) {
  res.setHeader('Content-Type', 'application/x-ndjson')
  res.setHeader('Transfer-Encoding', 'chunked')

  return {
    write(data) {
      res.write(JSON.stringify(data) + '\n')
    },
    end() {
      res.end()
    }
  }
}

/**
 * Read request body as string (with size limit to prevent OOM)
 */
const MAX_BODY_SIZE = 1024 * 1024 // 1MB

export async function readBody(req) {
  let body = ''
  let size = 0
  for await (const chunk of req) {
    size += chunk.length
    if (size > MAX_BODY_SIZE) {
      throw new Error('Request body too large (max 1MB)')
    }
    body += chunk.toString()
  }
  return body
}

/**
 * Read and parse request body as JSON
 */
export async function readJson(req) {
  const body = await readBody(req)
  if (!body) return {}
  return JSON.parse(body)
}

/**
 * Escape HTML for safe display
 */
export function escapeHtml(text) {
  if (!text) return ''
  return String(text)
    .replace(/&/g, '&')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;')
}

/**
 * Create redirect response
 */
export function redirect(res, url, status = 302) {
  res.statusCode = status
  res.setHeader('Location', url)
  res.end()
}