lib/response.js
var logging = require("./logging");
var config = require("../config");
var crc = require("crc").crc32;
var human_status = {
"-2": "user error", // e.g. invalid size
"-1": "server error", // e.g. mojang/network issues
0: "none", // cached as null (user has no skin)
1: "cached", // found on disk
2: "downloaded", // profile downloaded, skin downloaded from mojang servers
3: "checked", // profile re-downloaded (was too old), has no skin or skin cached
4: "server error;cached" // tried to check but ran into server error, using cached version
};
// print these, but without stacktrace
var silent_errors = ["ETIMEDOUT", "ESOCKETTIMEDOUT", "ECONNRESET", "EHOSTUNREACH", "ECONNREFUSED", "HTTPERROR", "RATELIMIT"];
// handles HTTP responses
// +request+ a http.IncomingMessage
// +response+ a http.ServerResponse
// +result+ an object with:
// * status: see human_status, required for images without err
// * redirect: redirect URL
// * body: file or message, required unless redirect is present or status is < 0
// * type: a valid Content-Type for the body, defaults to "text/plain"
// * hash: image hash, required when body is an image
// * err: a possible Error
// * code: override HTTP response code when status is < 0
module.exports = function(request, response, result) {
// These headers are the same for every response
var headers = {
"Content-Type": result.body && result.type || "text/plain",
"Content-Length": Buffer.from(result.body || "").length,
"Cache-Control": "max-age=" + config.caching.browser,
"Response-Time": Date.now() - request.start,
"X-Request-ID": request.id,
"Access-Control-Allow-Origin": "*",
};
response.on("finish", function() {
logging.log(request.id, request.method, request.url.href, response.statusCode, headers["Response-Time"] + "ms", "(" + (human_status[result.status] || "-") + ")");
});
response.on("error", function(err) {
logging.error(request.id, err);
});
if (result.err) {
var silent = silent_errors.indexOf(result.err.code) !== -1;
if (result.err.stack && !silent) {
logging.error(request.id, result.err.stack);
} else if (silent) {
logging.warn(request.id, result.err);
} else {
logging.error(request.id, result.err);
}
result.status = -1;
}
if (result.status !== undefined && result.status !== null) {
headers["X-Storage-Type"] = human_status[result.status];
}
// use crc32 as a hash function for Etag
var etag = "\"" + crc(result.body || "") + "\"";
// handle etag caching
var incoming_etag = request.headers["if-none-match"];
// also respond with 304 on server error (use client's version)
// don't respond with 304 when debugging is enabled
if (incoming_etag && (incoming_etag === etag || result.status === -1 && !config.server.debug_enabled)) {
response.writeHead(304, headers);
response.end();
return;
}
if (result.redirect) {
headers.Location = result.redirect;
response.writeHead(307, headers);
response.end();
return;
}
if (result.status === -2) {
response.writeHead(result.code || 422, headers);
} else if (result.status === -1) {
// server errors shouldn't be cached
headers["Cache-Control"] = "no-cache, max-age=0";
if (result.body && result.hash && !result.hash.startsWith("mhf_")) {
headers["Warning"] = '110 Crafatar "Response is Stale"'
headers["Etag"] = etag;
result.code = result.code || 200;
}
if (result.err && result.err.code === "ENOENT") {
result.code = result.code || 500;
}
if (!result.code) {
// Don't use 502 on Cloudflare
// As they will show their own error page instead
// https://support.cloudflare.com/hc/en-us/articles/200172706
result.code = config.caching.cloudflare ? 500 : 502;
}
response.writeHead(result.code, headers);
} else {
if (result.body) {
if (result.status === 4) {
headers["Warning"] = '111 Crafatar "Revalidation Failed"'
}
headers["Etag"] = etag;
response.writeHead(200, headers);
} else {
response.writeHead(404, headers);
}
}
response.end(result.body);
};