ltijs
Version:
Easily turn your web application into a LTI 1.3 Learning Tool.
157 lines • 7.01 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExpressHttpHandler = void 0;
const node_https_1 = require("node:https");
const express_1 = __importDefault(require("express"));
const cors_1 = __importDefault(require("cors"));
const helmet_1 = __importDefault(require("helmet"));
const errors_1 = require("#shared/errors");
const errors_2 = require("#services/request-handler/errors");
const express_http_response_1 = require("#services/http-handler/express/express-http-response");
const http_handler_types_1 = require("#services/http-handler/http-handler.types");
class ExpressHttpHandler {
LOG_COMPONENT = 'expressHttpHandler';
INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR';
logger;
port;
ssl;
server;
app;
constructor(logger, options) {
this.logger = logger;
this.port = options.port;
this.ssl = options.ssl;
this.app = (0, express_1.default)();
this.setupServer(options);
}
registerRoute(path, methods, handler) {
const adapter = this.buildAdapter(handler);
for (const method of methods)
this.app[this.toExpressMethod(method)](path, adapter);
}
async listen() {
await new Promise((resolve, reject) => {
this.server =
this.ssl === undefined ? this.app.listen(this.port) : (0, node_https_1.createServer)(this.ssl, this.app).listen(this.port);
this.server.once('listening', () => {
resolve();
});
this.server.once('error', (error) => {
reject(error);
});
});
}
async close() {
if (this.server === undefined)
return;
await new Promise((resolve, reject) => {
this.server?.close(error => {
if (error !== undefined) {
reject(error);
return;
}
resolve();
});
});
}
setupServer(options) {
this.app.use((0, helmet_1.default)({ frameguard: false, contentSecurityPolicy: false }));
if (options.cors !== false) {
const corsMiddleware = (0, cors_1.default)({
origin: options.cors?.origin ?? true,
credentials: options.cors?.credentials ?? true,
});
this.app.use(corsMiddleware);
this.app.options('*splat', corsMiddleware);
}
this.app.use(express_1.default.json());
this.app.use(express_1.default.urlencoded({ extended: false }));
// Catches errors thrown by the middleware above (a malformed JSON body, say) before any route ever
// runs. Route handlers already get this same mapping via buildAdapter()'s own try/catch, so this
// only needs to sit after the middleware registered here, regardless of what registerRoute() adds
// later: Express only propagates an error forward through the stack from where it was thrown, and a
// route handler's own errors never reach `next()` at all.
this.app.use((err, _req, res, _next) => {
this.handleRouteError(err, new express_http_response_1.ExpressHttpResponse(res));
});
}
buildAdapter(handler) {
return async (req, res) => {
try {
const request = this.buildRequestParameters(req);
const response = new express_http_response_1.ExpressHttpResponse(res);
await handler(request, response);
}
catch (error) {
this.handleRouteError(error, new express_http_response_1.ExpressHttpResponse(res));
}
};
}
handleRouteError(error, response) {
if (error instanceof errors_1.LtijsError) {
this.logger.error(this.LOG_COMPONENT, error.message);
response.status(400).json({ error: error.name, message: error.message });
return;
}
if (error instanceof errors_2.HttpError) {
this.logger.error(this.LOG_COMPONENT, error.message);
// The platform rejected the outbound call, so its own status best describes what went wrong (e.g. a
// 400 for a bad score submission). 502 is only a fallback for when the call failed before a status
// was ever received (e.g. a network error). `external: true` tells the caller this status came from
// the platform, not from this tool validating the caller's own request.
response.status(error.status ?? 502).json({
error: error.name,
message: error.message,
external: true,
platformResponse: error.response,
});
return;
}
const message = error instanceof Error ? error.message : String(error);
this.logger.error(this.LOG_COMPONENT, message);
// A handful of libraries (body-parser among them, for a malformed request body) throw a plain Error
// carrying its own `.status`/`.statusCode`, deliberately meant to be shown to the caller. Trusting
// only the 4xx range keeps a genuine 5xx-ish or absent status falling through to the generic case
// below, rather than exposing an arbitrary internal error's shape to the client.
const clientErrorStatus = this.resolveClientErrorStatus(error);
if (clientErrorStatus !== undefined && error instanceof Error) {
response.status(clientErrorStatus).json({ error: error.name, message });
return;
}
response.status(500).json({ error: this.INTERNAL_SERVER_ERROR });
}
resolveClientErrorStatus(error) {
if (typeof error !== 'object' || error === null)
return undefined;
const { status, statusCode } = error;
const candidate = typeof status === 'number' ? status : statusCode;
if (typeof candidate !== 'number' || candidate < 400 || candidate >= 500)
return undefined;
return candidate;
}
buildRequestParameters(req) {
return {
method: req.method,
path: req.path,
query: req.query,
body: req.body ?? {},
headers: req.headers,
};
}
toExpressMethod(method) {
const EXPRESS_METHOD_BY_HTTP_METHOD = {
[http_handler_types_1.HttpMethod.Get]: 'get',
[http_handler_types_1.HttpMethod.Post]: 'post',
[http_handler_types_1.HttpMethod.Put]: 'put',
[http_handler_types_1.HttpMethod.Delete]: 'delete',
[http_handler_types_1.HttpMethod.All]: 'all',
};
return EXPRESS_METHOD_BY_HTTP_METHOD[method];
}
}
exports.ExpressHttpHandler = ExpressHttpHandler;
exports.default = ExpressHttpHandler;
//# sourceMappingURL=express-http-handler.service.js.map