@itwin/core-backend
Version:
iTwin.js backend components
139 lines • 7.21 kB
JavaScript
;
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module RpcInterface
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.sendResponse = sendResponse;
const core_bentley_1 = require("@itwin/core-bentley");
const core_common_1 = require("@itwin/core-common");
const node_stream_1 = require("node:stream");
const node_util_1 = require("node:util");
const node_zlib_1 = require("node:zlib");
/* eslint-disable @typescript-eslint/no-deprecated */
function configureResponse(protocol, request, fulfillment, res) {
const success = protocol.getStatus(fulfillment.status) === core_common_1.RpcRequestStatus.Resolved;
// TODO: Use stale-while-revalidate in cache headers. This needs to be tested, and does not currently have support in the router/caching-service.
// This will allow browsers to use stale cached responses while also revalidating with the router, allowing us to start up a backend if necessary.
// RPC Caching Service uses the s-maxage header to determine the TTL for the redis cache.
const oneHourInSeconds = 3600;
if (success && request.caching === core_common_1.RpcResponseCacheControl.Immutable) {
// If response size is > 50 MB, do not cache it.
if (fulfillment.result.objects.length > (50 * 10 ** 7)) {
res.set("Cache-Control", "no-store");
}
else if (request.operation.operationName === "generateTileContent") {
res.set("Cache-Control", "no-store");
}
else if (request.operation.operationName === "getConnectionProps") {
// GetConnectionprops can't be cached on the browser longer than the lifespan of the backend. The lifespan of backend may shrink too. Keep it at 1 second to be safe.
res.set("Cache-Control", `s-maxage=${oneHourInSeconds * 24}, max-age=1, immutable`);
}
else if (request.operation.operationName === "getTileCacheContainerUrl") {
// getTileCacheContainerUrl returns a SAS with an expiry of 23:59:59. We can't exceed that time when setting the max-age.
res.set("Cache-Control", `s-maxage=${oneHourInSeconds * 23}, max-age=${oneHourInSeconds * 23}, immutable`);
}
else {
res.set("Cache-Control", `s-maxage=${oneHourInSeconds * 24}, max-age=${oneHourInSeconds * 48}, immutable`);
}
}
if (fulfillment.retry) {
res.set("Retry-After", fulfillment.retry);
}
}
function configureText(fulfillment, res) {
res.set(core_common_1.WEB_RPC_CONSTANTS.CONTENT, core_common_1.WEB_RPC_CONSTANTS.TEXT);
return (fulfillment.status === 204) ? "" : fulfillment.result.objects;
}
function configureBinary(fulfillment, res) {
res.set(core_common_1.WEB_RPC_CONSTANTS.CONTENT, core_common_1.WEB_RPC_CONSTANTS.BINARY);
const data = fulfillment.result.data[0];
return Buffer.isBuffer(data) ? data : Buffer.from(data);
}
function configureMultipart(fulfillment, res) {
const response = core_common_1.RpcMultipart.createStream(fulfillment.result);
const headers = response.getHeaders();
for (const header in headers) {
if (headers.hasOwnProperty(header)) {
res.set(header, headers[header]);
}
}
return response;
}
function configureStream(fulfillment) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return fulfillment.result.stream;
}
async function configureEncoding(req, res, responseBody) {
const acceptedEncodings = req.header("Accept-Encoding")?.split(",").map((value) => value.trim());
if (!acceptedEncodings)
return responseBody;
const encoding = acceptedEncodings.includes("br") ? "br" : acceptedEncodings.includes("gzip") ? "gzip" : undefined;
if (!encoding)
return responseBody;
res.set("Content-Encoding", encoding);
const brotliOptions = {
params: {
// Experimentation revealed that the default compression quality significantly increases the compression time for larger texts.
// Reducing the quality improves speed substantially without a significant loss in the compression ratio.
[node_zlib_1.constants.BROTLI_PARAM_QUALITY]: 3,
},
};
if (responseBody instanceof node_stream_1.Stream) {
const compressStream = encoding === "br" ? (0, node_zlib_1.createBrotliCompress)(brotliOptions) : (0, node_zlib_1.createGzip)();
return responseBody.pipe(compressStream);
}
return encoding === "br" ? (0, node_util_1.promisify)(node_zlib_1.brotliCompress)(responseBody, brotliOptions) : (0, node_util_1.promisify)(node_zlib_1.gzip)(responseBody);
}
/** @internal */
async function sendResponse(protocol, request, fulfillment, req, res) {
logResponse(request, fulfillment.status, fulfillment.rawResult);
const versionHeader = protocol.protocolVersionHeaderName;
if (versionHeader && core_common_1.RpcProtocol.protocolVersion) {
res.set(versionHeader, core_common_1.RpcProtocol.protocolVersion.toString());
}
const transportType = core_common_1.WebAppRpcRequest.computeTransportType(fulfillment.result, fulfillment.rawResult);
let responseBody;
if (transportType === core_common_1.RpcContentType.Binary) {
responseBody = configureBinary(fulfillment, res);
}
else if (transportType === core_common_1.RpcContentType.Multipart) {
responseBody = configureMultipart(fulfillment, res);
}
else if (transportType === core_common_1.RpcContentType.Stream) {
responseBody = configureStream(fulfillment);
}
else {
responseBody = configureText(fulfillment, res);
}
configureResponse(protocol, request, fulfillment, res);
res.status(fulfillment.status);
if (fulfillment.allowCompression)
responseBody = await configureEncoding(req, res, responseBody);
// This check should in theory look for instances of Readable, but that would break backend implementation at
// core/backend/src/RpcBackend.ts
if (responseBody instanceof node_stream_1.Stream) {
responseBody.pipe(res);
}
else {
res.send(responseBody);
}
}
function logResponse(request, statusCode, resultObj) {
const metadata = {
ActivityId: request.id, // eslint-disable-line @typescript-eslint/naming-convention
method: request.method,
path: request.path,
operation: request.operation,
statusCode,
errorObj: resultObj instanceof Error ? core_bentley_1.BentleyError.getErrorProps(resultObj) : undefined,
};
if (statusCode < 400)
core_bentley_1.Logger.logInfo(core_common_1.CommonLoggerCategory.RpcInterfaceBackend, "RPC over HTTP success response", metadata);
else
core_bentley_1.Logger.logError(core_common_1.CommonLoggerCategory.RpcInterfaceBackend, "RPC over HTTP failure response", metadata);
}
//# sourceMappingURL=response.js.map