gracenode-server
Version:
server module for gracenode framework.
377 lines (309 loc) • 10.7 kB
JavaScript
var fs = require('fs');
var gracenode = require('../../gracenode');
var log = gracenode.log.create('server-response');
var hook = require('../hooks/response');
var zlib = require('zlib');
var mime = require('./mime');
var defaultHeaders = {
'Cache-Control': 'no-cache, must-revalidate',
'Connection': 'Keep-Alive',
'Pragma': 'no-cache', // for old client that does not support cache-control header
'Vary': 'Accept-Encoding',
};
module.exports.create = function (resource, requestObj) {
return new Response(resource, requestObj);
};
function Response(resource, requestObj) {
this._resource = resource;
this._requestObj = requestObj;
this._defaultStatus = 200;
this._resHeaders = {};
for (var name in defaultHeaders) {
this._resHeaders[name] = defaultHeaders[name];
}
if (requestObj.headers.isAcceptedEncoding('gzip')) {
this._resHeaders['Content-Encoding'] = 'gzip';
}
}
Response.prototype.getRequest = function () {
return this._requestObj;
};
Response.prototype.header = function (name, value) {
if (value === null || value === undefined) {
delete this._resHeaders[name];
return;
}
this._resHeaders[name] = value;
};
Response.prototype.json = function (content, status) {
log.verbose('response content type: JSON');
this.respond(this._resHeaders, respondJSON, content, status);
};
Response.prototype.html = function (content, status) {
log.verbose('response content type: HTML');
this.respond(this._resHeaders, respondHTML, content, status);
};
Response.prototype.data = function (content, status) {
log.verbose('response content type: Data');
this.respond(this._resHeaders, respondData, content, status);
};
Response.prototype.file = function (content, status) {
log.verbose('response content type: File');
delete this._resHeaders['Content-Encoding'];
this.respond(this._resHeaders, respondFILE, content, status);
};
Response.prototype.stream = function (contentPath, contentType) {
log.verbose('response content type: Stream');
delete this._resHeaders['Content-Encoding'];
this.respond(this._resHeaders, respondStream, contentPath, contentType);
};
Response.prototype.error = function (content, status) {
this._errorHandler(content, status);
};
Response.prototype.download = function (content, fileName, status) {
var fileType = fileName.substring(fileName.lastIndexOf('.') + 1);
log.verbose('response content type:', fileType);
this._resHeaders['Content-Description'] = 'File Transfer';
this._resHeaders['Content-Disposition'] = 'attachment; filename=' + fileName;
this._resHeaders['Content-Type'] = mime.get(fileType);
this.respond(this._resHeaders, respondDownload, content, status);
};
Response.prototype.redirect = function (content, status) {
log.verbose('response content type: Redirect');
this.respond(this._resHeaders, respondRedirect, content, status || 307);
};
// internal use only
Response.prototype._setDefaultStatus = function (status) {
this._defaultStatus = status;
};
Response.prototype.respond = function (headers, respondFunc, content, status) {
// check for duplicated response
if (this._resource.responded) {
var rawReq = this._resource.rawRequest;
var dupResError = new Error(
'responded more than once: (url:' + rawReq.url + ') (request-id:' + rawReq.uniqueId + ')'
);
log.error(dupResError, 'content:', content, 'stauts:', (status || 200));
// do not send response to the client anymore
return;
}
var that = this;
hook.exec(this, function () {
// this callback will NOT be executed on error of the hook
// now respond
setupFinish(
that._resource.rawRequest,
that._resource.rawResponse,
that._resource.server,
that._resource.startTime
);
respondFunc(
headers,
that._resource.rawRequest,
that._resource.rawResponse,
content, status || that._defaultStatus
);
finish(that._resource.rawRequest, that._resource.rawResponse, that._resource.server);
that._resource.responded = true;
});
};
// overrriden by controller
Response.prototype._errorHandler = function () {
};
Response.prototype._error = function (content, status) {
this.respond(this._resHeaders, respondERROR, content, status);
};
// sets up events for response finish. The events will be called when the request response has all been sent.
function setupFinish(req, res, server, startTime) {
// this will be called when the server sends the response data and finishes it.
res.once('finish', function () {
var execTime = Date.now() - startTime;
var msg = 'request responded: (url:' + req.url + ') (request-id:' + req.uniqueId;
msg += ') (took:' + execTime + 'ms) (status:' + res.statusCode + ')';
if (res.statusCode > 399) {
log.error(msg);
} else {
log.info(msg);
}
server.emit('requestFinish', req.url, execTime, res.statusCode);
});
}
function finish(req, res, server) {
res.emit('end', req.url);
// this will be called when the server finishes all operation (not when the response data sent)
server.emit('requestEnd', req.url);
}
function compressContent(req, content, headers, cb) {
if (headers['Content-Encoding'] !== 'gzip') {
// the client did not request compression
return cb(null, content);
}
if (content instanceof Buffer) {
// we do not compress binary
log.verbose('skip compressing binary data: (url:' + req.url + ') ' + (content.length / 1024) + 'KB');
return cb(null, content);
}
zlib.gzip(content, function (error, compressedData) {
if (error) {
return cb(error);
}
log.verbose(
'compressed content size: (url:' + req.url + ') ' + (compressedData.length / 1024) + ' KB'
);
cb(null, compressedData);
});
}
// this function does NOT allow to overwrite headers object
function setupHeaders(res, status, headers, additionalHeaders) {
for (var name in additionalHeaders) {
if (!headers.hasOwnProperty(name)) {
headers[name] = additionalHeaders[name];
}
}
res.writeHead(status, headers);
}
function respondJSON(headers, req, res, content, status) {
content = content || null;
compressContent(req, JSON.stringify(content), headers, function (error, data) {
if (error) {
log.error('compression error: (url:' + req.url, + ')', error);
status = 500;
data = error;
}
setupHeaders(res, status, headers, {
'Content-Type': 'application/json; charset=UTF-8',
'Cache-Control': 'no-cache, must-revalidate',
'Content-Length': data.length
});
sendResponse(req, res, data, 'binary');
});
}
function respondHTML(headers, req, res, content, status) {
content = content || null;
compressContent(req, content, headers, function (error, data) {
if (error) {
log.error('compression error: (url:' + req.url, + ')', error);
status = 500;
data = error;
}
setupHeaders(res, status, headers, {
'Content-Type': 'text/html; charset=UTF-8',
'Content-Length': data.length
});
sendResponse(req, res, data, 'binary');
});
}
function respondData(headers, req, res, content, status) {
content = content || null;
compressContent(req, content, headers, function (error, data) {
if (error) {
log.error('compression error: (url:' + req.url + ')', error);
status = 500;
data = error;
}
setupHeaders(res, status, headers, {
'Content-Type': 'text/plain; charset=UTF-8',
'Content-Length': data.length
});
sendResponse(req, res, data, 'binary');
});
}
function respondDownload(headers, req, res, content, status) {
content = content || null;
compressContent(req, content, headers, function (error, data) {
if (error) {
log.error('compression error: (url:' + req.url + ')', error);
status = 500;
data = error;
}
setupHeaders(res, status, headers, {
'Content-Length': data.length
});
sendResponse(req, res, data, 'binary');
});
}
function respondRedirect(headers, req, res, content, status) {
content = content || null;
// content needs to be redirect URL
setupHeaders(res, status, headers, {
'Location': content
});
log.verbose('redirect to: ', content);
res.end();
}
function respondFILE(headers, req, res, content, status) {
content = content || null;
var type = req.url.substring(req.url.lastIndexOf('.') + 1);
var contentSize = content.length;
setupHeaders(res, status, headers, {
'Content-Length': contentSize,
'Content-Type': mime.get(type)
});
log.verbose('response content size: (url:' + req.url + ') ' + (contentSize / 1024) + ' KB');
sendResponse(req, res, content, 'binary');
}
function respondERROR(headers, req, res, content, status) {
var contentType = 'text/plain; charset=UTF-8';
content = content || null;
if (content !== null && typeof content === 'object') {
content = JSON.stringify(content);
contentType = 'application/json; charset=UTF-8';
}
status = status || 400;
compressContent(req, content, headers, function (error, data) {
if (error) {
log.error('(url:' + req.url + ')', error);
status = 500;
data = error;
}
var contentSize = data.length;
setupHeaders(res, status, headers, {
'Content-Type': contentType,
'Content-Length': data.length
});
log.verbose('response content size: (url:' + req.url + ') ' + (contentSize / 1024) + ' KB');
sendResponse(req, res, data, 'binary');
});
}
function respondStream(headers, req, res, path, type) {
fs.stat(path, function (error, stat) {
if (error) {
log.error('(url:' + req.url + ')', error);
respondERROR(headers, req, res, error, 404);
return;
}
var total = stat.size;
var wstream = res;
if (req.headers.range) {
var range = req.headers.range;
var parts = range.replace(/bytes=/, '').split('-');
var partialStart = parts[0];
var partialEnd = parts[1];
var start = parseInt(partialStart, 10);
var end = partialEnd ? parseInt(partialEnd, 10) : total - 1;
var chunkSize = (end - start) + 1;
log.verbose('streaming ranged content (url:' + req.url + ') range:', start + ' - ' + end + ' = ' + chunkSize);
var rstream = fs.createReadStream(path, { start: start, end: end });
wstream.writeHead(206, {
'Content-Range': 'bytes ' + start + '-' + end + '/' + total,
'Accept-Ranges': 'bytes',
'Content-Length': chunkSize,
'Content-Type': type
});
rstream.pipe(wstream);
} else {
log.verbose('serve the whole file (url:' + req.url + '):', total);
wstream.writeHead(200, { 'Content-Length': total, 'Content-Type': type });
fs.createReadStream(path).pipe(wstream);
}
});
}
function sendResponse(req, res, data, type) {
// send and end the response
if (req.method === 'HEAD') {
// we do not send response content with HEAD request method
return res.end('', 'binary');
}
log.verbose('response headers:', res._header);
res.end(data, type);
}