@itwin/core-common
Version:
iTwin.js components common to frontend and backend
236 lines • 9.96 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 { BentleyStatus, expectNotNull } from "@itwin/core-bentley";
import { IModelError, ServerError, ServerTimeoutError } from "../../IModelError";
import { RpcContentType, RpcProtocolEvent, WEB_RPC_CONSTANTS } from "../core/RpcConstants";
import { MarshalingBinaryMarker, RpcSerializedValue } from "../core/RpcMarshaling";
import { RpcRequest } from "../core/RpcRequest";
import { RpcMultipartParser } from "./multipart/RpcMultipartParser";
import { RpcMultipart } from "./RpcMultipart";
import { WebAppRpcProtocol } from "./WebAppRpcProtocol";
/** A web application RPC request.
* @internal
*/
export class WebAppRpcRequest extends RpcRequest {
_loading = false;
_request = {};
_pathSuffix = "";
get _headers() { return this._request.headers; }
/** The maximum size permitted for an encoded component in a URL.
* Note that some backends limit the total cumulative request size. Our current node backends accept requests with a max size of 16 kb.
* In addition to the url size, an authorization header may also add considerably to the request size.
* @note This is used for features like encoding the payload of a cacheable request in the URL.
*/
static maxUrlComponentSize = 1024 * 8;
/** The HTTP method for this request. */
method;
/** Convenience access to the protocol of this request. */
protocol = this.client.configuration.protocol;
/** Standardized access to metadata about the request (useful for purposes such as logging). */
metadata = { status: 0, message: "" };
/** Parses a request. */
static async parseRequest(protocol, req) {
return this.backend.parseRequest(protocol, req);
}
/** Sends the response for a web request. */
static async sendResponse(protocol, request, fulfillment, req, res) {
return this.backend.sendResponse(protocol, request, fulfillment, req, res);
}
/** Determines the most efficient transport type for an RPC value. */
static computeTransportType(value, source) {
if (source instanceof Uint8Array || (Array.isArray(source) && source[0] instanceof Uint8Array)) {
return RpcContentType.Binary;
}
else if (value.data.length > 0) {
return RpcContentType.Multipart;
}
else if (value.stream) {
return RpcContentType.Stream;
}
else {
return RpcContentType.Text;
}
}
/** Constructs a web application request. */
constructor(client, operation, parameters) {
super(client, operation, parameters);
this.path = this.protocol.supplyPathForOperation(this.operation, this);
this.method = "head";
this._request.headers = {};
}
/** Sets request header values. */
setHeader(name, value) {
this._headers[name] = value;
}
/** Sends the request. */
async send() {
this._loading = true;
await this.setupTransport();
return new Promise(async (resolve, reject) => {
try {
resolve(await this.performFetch());
}
catch (reason) {
reject(new ServerError(-1, typeof (reason) === "string" ? reason : "Server connection error."));
}
});
}
computeRetryAfter(attempts) {
const retryAfter = this._response && this._response.headers.get("Retry-After");
if (retryAfter) {
this.resetTransientFaultCount();
const r = Number(retryAfter);
if (Number.isFinite(r)) {
return r * 1000;
}
const d = Date.parse(retryAfter);
if (!Number.isNaN(d)) {
return d - Date.now();
}
}
else {
this.recordTransientFault();
}
return super.computeRetryAfter(attempts);
}
handleUnknownResponse(code) {
if (this.protocol.isTimeout(code)) {
this.reject(new ServerTimeoutError("Request timeout."));
}
else {
this.reject(new ServerError(code, "Unknown server response code."));
}
}
async load() {
return new Promise(async (resolve, reject) => {
try {
if (!this._loading)
return;
const response = this._response;
if (!response) {
reject(new IModelError(BentleyStatus.ERROR, "Invalid state."));
return;
}
if (this.protocol.protocolVersionHeaderName) {
const version = response.headers.get(this.protocol.protocolVersionHeaderName);
if (version) {
this.responseProtocolVersion = parseInt(version, 10);
}
}
const contentType = response.headers.get(WEB_RPC_CONSTANTS.CONTENT);
const responseType = WebAppRpcProtocol.computeContentType(contentType);
if (responseType === RpcContentType.Binary) {
resolve(await this.loadBinary(response));
}
else if (responseType === RpcContentType.Multipart) {
resolve(await this.loadMultipart(response, expectNotNull(contentType)));
}
else {
resolve(await this.loadText(response));
}
this._loading = false;
this.setLastUpdatedTime();
this.protocol.events.raiseEvent(RpcProtocolEvent.ResponseLoaded, this);
}
catch (reason) {
if (!this._loading)
return;
this._loading = false;
reject(new ServerError(this.metadata.status, typeof (reason) === "string" ? reason : "Unknown server response error."));
}
});
}
/** Override to supply an alternate fetch function. */
supplyFetch() {
return fetch;
}
/** Override to supply an alternate Request function. */
supplyRequest() {
return Request;
}
async performFetch() {
const requestClass = this.supplyRequest();
const fetchFunction = this.supplyFetch();
const path = new URL(this.path, typeof location !== "undefined" ? location.origin : undefined);
if (this._pathSuffix) {
const params = new URLSearchParams();
params.set("parameters", this._pathSuffix);
path.search = `?${params.toString()}`;
}
const request = new requestClass(path.toString(), this._request);
const response = await fetchFunction(request);
this._response = response;
this.metadata.status = response.status;
return response.status;
}
async loadText(response) {
const value = await response.text();
this.metadata.message = value;
return RpcSerializedValue.create(value);
}
async loadBinary(response) {
const value = new Uint8Array(await response.arrayBuffer());
const objects = JSON.stringify(MarshalingBinaryMarker.createDefault());
return RpcSerializedValue.create(objects, [value]);
}
async loadMultipart(response, contentType) {
const data = await response.arrayBuffer();
const value = new RpcMultipartParser(contentType, new Uint8Array(data)).parse();
return value;
}
async setupTransport() {
const parameters = (await this.protocol.serialize(this)).parameters;
const transportType = WebAppRpcRequest.computeTransportType(parameters, this.parameters);
if (transportType === RpcContentType.Binary) {
this.setupBinaryTransport(parameters);
}
else if (transportType === RpcContentType.Multipart) {
this.setupMultipartTransport(parameters);
}
else {
this.setupTextTransport(parameters);
}
}
setupBinaryTransport(parameters) {
this._headers[WEB_RPC_CONSTANTS.CONTENT] = WEB_RPC_CONSTANTS.BINARY;
this._request.method = "post";
this._request.body = parameters.data[0];
}
setupMultipartTransport(parameters) {
// IMPORTANT: do not set a multipart Content-Type header value. The browser does this automatically!
delete this._headers[WEB_RPC_CONSTANTS.CONTENT];
this._request.method = "post";
this._request.body = RpcMultipart.createForm(parameters);
}
setupTextTransport(parameters) {
if (this.operation.policy.allowResponseCaching(this)) {
const encodedBody = btoa(parameters.objects);
if (encodedBody.length <= WebAppRpcRequest.maxUrlComponentSize) {
this._request.method = "get";
this._request.body = undefined;
delete this._headers[WEB_RPC_CONSTANTS.CONTENT];
this._pathSuffix = encodedBody;
return;
}
}
this._pathSuffix = "";
this._headers[WEB_RPC_CONSTANTS.CONTENT] = WEB_RPC_CONSTANTS.TEXT;
this._request.method = "post";
this._request.body = parameters.objects;
}
/** @internal */
static backend = {
sendResponse: async (_protocol, _request, _fulfillment, _req, _res) => {
throw new IModelError(BentleyStatus.ERROR, "Not bound.");
},
parseRequest: async (_protocol, _req) => {
throw new IModelError(BentleyStatus.ERROR, "Not bound.");
},
};
}
//# sourceMappingURL=WebAppRpcRequest.js.map