@oxog/spark
Version:
Ultra-fast, zero-dependency Node.js web framework with security hardening, memory leak protection, and enhanced error handling
375 lines (302 loc) • 8.14 kB
JavaScript
const { STATUS_CODES } = require('http');
const path = require('path');
const fs = require('fs');
const { promisify } = require('util');
const stat = promisify(fs.stat);
const readFile = promisify(fs.readFile);
class Response {
constructor(res) {
this.res = res;
this.statusCode = 200;
this.headers = {};
this.locals = {};
this.finished = false;
this.headersSent = false;
}
status(code) {
this.statusCode = code;
return this;
}
set(field, value) {
if (typeof field === 'object') {
Object.assign(this.headers, field);
} else {
this.headers[field.toLowerCase()] = value;
}
return this;
}
get(field) {
return this.headers[field.toLowerCase()];
}
header(field, value) {
return this.set(field, value);
}
setHeader(name, value) {
return this.set(name, value);
}
getHeader(name) {
return this.get(name);
}
removeHeader(name) {
delete this.headers[name.toLowerCase()];
return this;
}
append(field, value) {
const existing = this.get(field);
if (existing) {
this.set(field, Array.isArray(existing) ? existing.concat(value) : [existing, value]);
} else {
this.set(field, value);
}
return this;
}
cookie(name, value, options = {}) {
let cookieString = `${name}=${encodeURIComponent(value)}`;
if (options.domain) {
cookieString += `; Domain=${options.domain}`;
}
if (options.path) {
cookieString += `; Path=${options.path}`;
}
if (options.expires) {
cookieString += `; Expires=${options.expires.toUTCString()}`;
}
if (options.maxAge) {
cookieString += `; Max-Age=${options.maxAge}`;
}
if (options.httpOnly) {
cookieString += '; HttpOnly';
}
if (options.secure) {
cookieString += '; Secure';
}
if (options.sameSite) {
cookieString += `; SameSite=${options.sameSite}`;
}
this.append('Set-Cookie', cookieString);
return this;
}
clearCookie(name, options = {}) {
const clearOptions = {
...options,
expires: new Date(0),
maxAge: 0
};
return this.cookie(name, '', clearOptions);
}
redirect(url, status = 302) {
this.status(status);
this.set('Location', url);
this.end();
return this;
}
location(url) {
this.set('Location', url);
return this;
}
links(links) {
let linkHeader = this.get('Link') || '';
Object.keys(links).forEach(rel => {
if (linkHeader) linkHeader += ', ';
linkHeader += `<${links[rel]}>; rel="${rel}"`;
});
this.set('Link', linkHeader);
return this;
}
type(type) {
const mimeTypes = {
html: 'text/html',
json: 'application/json',
xml: 'application/xml',
text: 'text/plain',
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
gif: 'image/gif',
css: 'text/css',
js: 'application/javascript',
pdf: 'application/pdf'
};
const contentType = mimeTypes[type] || type;
this.set('Content-Type', contentType);
return this;
}
format(obj) {
const keys = Object.keys(obj);
const accepts = this.req.accepts(keys);
if (accepts) {
this.type(accepts);
obj[accepts](this.req, this);
} else {
this.status(406).send('Not Acceptable');
}
return this;
}
attachment(filename) {
if (filename) {
this.type(path.extname(filename));
this.set('Content-Disposition', `attachment; filename="${filename}"`);
} else {
this.set('Content-Disposition', 'attachment');
}
return this;
}
json(obj) {
this.type('json');
this.send(JSON.stringify(obj));
return this;
}
jsonp(obj) {
const callback = this.req.query.callback || 'callback';
this.type('text/javascript');
this.send(`${callback}(${JSON.stringify(obj)})`);
return this;
}
send(body) {
if (this.finished) {
return this;
}
this.writeHead();
if (body === null || body === undefined) {
this.res.end();
} else if (typeof body === 'string') {
if (!this.get('Content-Type')) {
this.type('html');
}
this.res.end(body);
} else if (Buffer.isBuffer(body)) {
if (!this.get('Content-Type')) {
this.type('application/octet-stream');
}
this.res.end(body);
} else {
this.json(body);
}
this.finished = true;
return this;
}
async sendFile(filePath, options = {}) {
try {
const stats = await stat(filePath);
if (!stats.isFile()) {
return this.status(404).send('File not found');
}
const ext = path.extname(filePath);
this.type(ext);
if (options.maxAge) {
this.set('Cache-Control', `public, max-age=${options.maxAge}`);
}
this.set('Content-Length', stats.size);
this.set('Last-Modified', stats.mtime.toUTCString());
const etag = `"${stats.size}-${stats.mtime.getTime()}"`;
this.set('ETag', etag);
if (this.req.headers['if-none-match'] === etag) {
return this.status(304).end();
}
if (options.headers) {
this.set(options.headers);
}
this.writeHead();
const stream = fs.createReadStream(filePath);
stream.pipe(this.res);
stream.on('end', () => {
this.finished = true;
});
stream.on('error', (error) => {
this.status(500).send('Internal Server Error');
});
} catch (error) {
this.status(404).send('File not found');
}
return this;
}
async sendStatus(statusCode) {
this.status(statusCode);
this.send(STATUS_CODES[statusCode] || 'Unknown Status');
return this;
}
download(filePath, filename, options = {}) {
const name = filename || path.basename(filePath);
this.attachment(name);
return this.sendFile(filePath, options);
}
render(view, locals = {}, callback) {
if (typeof locals === 'function') {
callback = locals;
locals = {};
}
const renderLocals = { ...this.locals, ...locals };
if (this.app.render) {
this.app.render(view, renderLocals, (err, html) => {
if (err) {
if (callback) return callback(err);
throw err;
}
this.send(html);
if (callback) callback(null, html);
});
} else {
const error = new Error('No template engine configured');
if (callback) return callback(error);
throw error;
}
return this;
}
vary(field) {
this.append('Vary', field);
return this;
}
writeHead() {
if (this.headersSent) {
return;
}
this.res.statusCode = this.statusCode;
Object.keys(this.headers).forEach(name => {
this.res.setHeader(name, this.headers[name]);
});
this.headersSent = true;
}
write(chunk, encoding) {
if (!this.headersSent) {
this.writeHead();
}
return this.res.write(chunk, encoding);
}
end(data, encoding) {
if (!this.headersSent) {
this.writeHead();
}
this.finished = true;
this.res.end(data, encoding);
return this;
}
destroy() {
this.res.destroy();
return this;
}
setTimeout(msecs, callback) {
this.res.setTimeout(msecs, callback);
return this;
}
headersSent() {
return this.headersSent;
}
writableEnded() {
return this.res.writableEnded;
}
writable() {
return this.res.writable;
}
toString() {
return `Response ${this.statusCode}`;
}
toJSON() {
return {
statusCode: this.statusCode,
headers: this.headers,
finished: this.finished,
headersSent: this.headersSent
};
}
}
module.exports = Response;