/**
* 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, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
}
/**
* Create redirect response
*/
export function redirect(res, url, status = 302) {
res.statusCode = status
res.setHeader('Location', url)
res.end()
}