@itwin/core-backend
Version:
iTwin.js backend components
136 lines • 6.92 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
*/
import { BentleyError, Logger } from "@itwin/core-bentley";
import { CommonLoggerCategory, RpcContentType, RpcMultipart, RpcProtocol, RpcRequestStatus, RpcResponseCacheControl, WEB_RPC_CONSTANTS, WebAppRpcRequest, } from "@itwin/core-common";
import { Stream } from "node:stream";
import { promisify } from "node:util";
import { brotliCompress, createBrotliCompress, createGzip, gzip, constants as zlibConstants } from "node:zlib";
/* eslint-disable @typescript-eslint/no-deprecated */
function configureResponse(protocol, request, fulfillment, res) {
const success = protocol.getStatus(fulfillment.status) === 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 === 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(WEB_RPC_CONSTANTS.CONTENT, WEB_RPC_CONSTANTS.TEXT);
return (fulfillment.status === 204) ? "" : fulfillment.result.objects;
}
function configureBinary(fulfillment, res) {
res.set(WEB_RPC_CONSTANTS.CONTENT, WEB_RPC_CONSTANTS.BINARY);
const data = fulfillment.result.data[0];
return Buffer.isBuffer(data) ? data : Buffer.from(data);
}
function configureMultipart(fulfillment, res) {
const response = 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.
[zlibConstants.BROTLI_PARAM_QUALITY]: 3,
},
};
if (responseBody instanceof Stream) {
const compressStream = encoding === "br" ? createBrotliCompress(brotliOptions) : createGzip();
return responseBody.pipe(compressStream);
}
return encoding === "br" ? promisify(brotliCompress)(responseBody, brotliOptions) : promisify(gzip)(responseBody);
}
/** @internal */
export async function sendResponse(protocol, request, fulfillment, req, res) {
logResponse(request, fulfillment.status, fulfillment.rawResult);
const versionHeader = protocol.protocolVersionHeaderName;
if (versionHeader && RpcProtocol.protocolVersion) {
res.set(versionHeader, RpcProtocol.protocolVersion.toString());
}
const transportType = WebAppRpcRequest.computeTransportType(fulfillment.result, fulfillment.rawResult);
let responseBody;
if (transportType === RpcContentType.Binary) {
responseBody = configureBinary(fulfillment, res);
}
else if (transportType === RpcContentType.Multipart) {
responseBody = configureMultipart(fulfillment, res);
}
else if (transportType === 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 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 ? BentleyError.getErrorProps(resultObj) : undefined,
};
if (statusCode < 400)
Logger.logInfo(CommonLoggerCategory.RpcInterfaceBackend, "RPC over HTTP success response", metadata);
else
Logger.logError(CommonLoggerCategory.RpcInterfaceBackend, "RPC over HTTP failure response", metadata);
}
//# sourceMappingURL=response.js.map