link
This commit is contained in:
590
node_modules/firebase-functions/lib/common/providers/https.js
generated
vendored
Normal file
590
node_modules/firebase-functions/lib/common/providers/https.js
generated
vendored
Normal file
@@ -0,0 +1,590 @@
|
||||
"use strict";
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2021 Firebase
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.onCallHandler = exports.checkAuthToken = exports.unsafeDecodeAppCheckToken = exports.unsafeDecodeIdToken = exports.unsafeDecodeToken = exports.decode = exports.encode = exports.isValidRequest = exports.HttpsError = exports.DEFAULT_HEARTBEAT_SECONDS = exports.ORIGINAL_AUTH_HEADER = exports.CALLABLE_AUTH_HEADER = void 0;
|
||||
const cors = require("cors");
|
||||
const logger = require("../../logger");
|
||||
// TODO(inlined): Decide whether we want to un-version apps or whether we want a
|
||||
// different strategy
|
||||
const app_check_1 = require("firebase-admin/app-check");
|
||||
const auth_1 = require("firebase-admin/auth");
|
||||
const app_1 = require("../app");
|
||||
const debug_1 = require("../debug");
|
||||
const JWT_REGEX = /^[a-zA-Z0-9\-_=]+?\.[a-zA-Z0-9\-_=]+?\.([a-zA-Z0-9\-_=]+)?$/;
|
||||
/** @internal */
|
||||
exports.CALLABLE_AUTH_HEADER = "x-callable-context-auth";
|
||||
/** @internal */
|
||||
exports.ORIGINAL_AUTH_HEADER = "x-original-auth";
|
||||
/** @internal */
|
||||
exports.DEFAULT_HEARTBEAT_SECONDS = 30;
|
||||
/**
|
||||
* Standard error codes and HTTP statuses for different ways a request can fail,
|
||||
* as defined by:
|
||||
* https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto
|
||||
*
|
||||
* This map is used primarily to convert from a client error code string to
|
||||
* to the HTTP format error code string and status, and make sure it's in the
|
||||
* supported set.
|
||||
*/
|
||||
const errorCodeMap = {
|
||||
ok: { canonicalName: "OK", status: 200 },
|
||||
cancelled: { canonicalName: "CANCELLED", status: 499 },
|
||||
unknown: { canonicalName: "UNKNOWN", status: 500 },
|
||||
"invalid-argument": { canonicalName: "INVALID_ARGUMENT", status: 400 },
|
||||
"deadline-exceeded": { canonicalName: "DEADLINE_EXCEEDED", status: 504 },
|
||||
"not-found": { canonicalName: "NOT_FOUND", status: 404 },
|
||||
"already-exists": { canonicalName: "ALREADY_EXISTS", status: 409 },
|
||||
"permission-denied": { canonicalName: "PERMISSION_DENIED", status: 403 },
|
||||
unauthenticated: { canonicalName: "UNAUTHENTICATED", status: 401 },
|
||||
"resource-exhausted": { canonicalName: "RESOURCE_EXHAUSTED", status: 429 },
|
||||
"failed-precondition": { canonicalName: "FAILED_PRECONDITION", status: 400 },
|
||||
aborted: { canonicalName: "ABORTED", status: 409 },
|
||||
"out-of-range": { canonicalName: "OUT_OF_RANGE", status: 400 },
|
||||
unimplemented: { canonicalName: "UNIMPLEMENTED", status: 501 },
|
||||
internal: { canonicalName: "INTERNAL", status: 500 },
|
||||
unavailable: { canonicalName: "UNAVAILABLE", status: 503 },
|
||||
"data-loss": { canonicalName: "DATA_LOSS", status: 500 },
|
||||
};
|
||||
/**
|
||||
* An explicit error that can be thrown from a handler to send an error to the
|
||||
* client that called the function.
|
||||
*/
|
||||
class HttpsError extends Error {
|
||||
constructor(code, message, details) {
|
||||
super(message);
|
||||
// A sanity check for non-TypeScript consumers.
|
||||
if (code in errorCodeMap === false) {
|
||||
throw new Error(`Unknown error code: ${code}.`);
|
||||
}
|
||||
this.code = code;
|
||||
this.details = details;
|
||||
this.httpErrorCode = errorCodeMap[code];
|
||||
}
|
||||
/**
|
||||
* Returns a JSON-serializable representation of this object.
|
||||
*/
|
||||
toJSON() {
|
||||
const { details, httpErrorCode: { canonicalName: status }, message, } = this;
|
||||
return {
|
||||
...(details === undefined ? {} : { details }),
|
||||
message,
|
||||
status,
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.HttpsError = HttpsError;
|
||||
/** @hidden */
|
||||
// Returns true if req is a properly formatted callable request.
|
||||
function isValidRequest(req) {
|
||||
// The body must not be empty.
|
||||
if (!req.body) {
|
||||
logger.warn("Request is missing body.");
|
||||
return false;
|
||||
}
|
||||
// Make sure it's a POST.
|
||||
if (req.method !== "POST") {
|
||||
logger.warn("Request has invalid method.", req.method);
|
||||
return false;
|
||||
}
|
||||
// Check that the Content-Type is JSON.
|
||||
let contentType = (req.header("Content-Type") || "").toLowerCase();
|
||||
// If it has a charset, just ignore it for now.
|
||||
const semiColon = contentType.indexOf(";");
|
||||
if (semiColon >= 0) {
|
||||
contentType = contentType.slice(0, semiColon).trim();
|
||||
}
|
||||
if (contentType !== "application/json") {
|
||||
logger.warn("Request has incorrect Content-Type.", contentType);
|
||||
return false;
|
||||
}
|
||||
// The body must have data.
|
||||
if (typeof req.body.data === "undefined") {
|
||||
logger.warn("Request body is missing data.", req.body);
|
||||
return false;
|
||||
}
|
||||
// TODO(klimt): Allow only specific http headers.
|
||||
// Verify that the body does not have any extra fields.
|
||||
const extraKeys = Object.keys(req.body).filter((field) => field !== "data");
|
||||
if (extraKeys.length !== 0) {
|
||||
logger.warn("Request body has extra fields: ", extraKeys.join(", "));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
exports.isValidRequest = isValidRequest;
|
||||
/** @hidden */
|
||||
const LONG_TYPE = "type.googleapis.com/google.protobuf.Int64Value";
|
||||
/** @hidden */
|
||||
const UNSIGNED_LONG_TYPE = "type.googleapis.com/google.protobuf.UInt64Value";
|
||||
/**
|
||||
* Encodes arbitrary data in our special format for JSON.
|
||||
* This is exposed only for testing.
|
||||
*/
|
||||
/** @hidden */
|
||||
function encode(data) {
|
||||
if (data === null || typeof data === "undefined") {
|
||||
return null;
|
||||
}
|
||||
if (data instanceof Number) {
|
||||
data = data.valueOf();
|
||||
}
|
||||
if (Number.isFinite(data)) {
|
||||
// Any number in JS is safe to put directly in JSON and parse as a double
|
||||
// without any loss of precision.
|
||||
return data;
|
||||
}
|
||||
if (typeof data === "boolean") {
|
||||
return data;
|
||||
}
|
||||
if (typeof data === "string") {
|
||||
return data;
|
||||
}
|
||||
if (Array.isArray(data)) {
|
||||
return data.map(encode);
|
||||
}
|
||||
if (typeof data === "object" || typeof data === "function") {
|
||||
// Sadly we don't have Object.fromEntries in Node 10, so we can't use a single
|
||||
// list comprehension
|
||||
const obj = {};
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
obj[k] = encode(v);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
// If we got this far, the data is not encodable.
|
||||
logger.error("Data cannot be encoded in JSON.", data);
|
||||
throw new Error(`Data cannot be encoded in JSON: ${data}`);
|
||||
}
|
||||
exports.encode = encode;
|
||||
/**
|
||||
* Decodes our special format for JSON into native types.
|
||||
* This is exposed only for testing.
|
||||
*/
|
||||
/** @hidden */
|
||||
function decode(data) {
|
||||
if (data === null) {
|
||||
return data;
|
||||
}
|
||||
if (data["@type"]) {
|
||||
switch (data["@type"]) {
|
||||
case LONG_TYPE:
|
||||
// Fall through and handle this the same as unsigned.
|
||||
case UNSIGNED_LONG_TYPE: {
|
||||
// Technically, this could work return a valid number for malformed
|
||||
// data if there was a number followed by garbage. But it's just not
|
||||
// worth all the extra code to detect that case.
|
||||
const value = parseFloat(data.value);
|
||||
if (isNaN(value)) {
|
||||
logger.error("Data cannot be decoded from JSON.", data);
|
||||
throw new Error(`Data cannot be decoded from JSON: ${data}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
default: {
|
||||
logger.error("Data cannot be decoded from JSON.", data);
|
||||
throw new Error(`Data cannot be decoded from JSON: ${data}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Array.isArray(data)) {
|
||||
return data.map(decode);
|
||||
}
|
||||
if (typeof data === "object") {
|
||||
const obj = {};
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
obj[k] = decode(v);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
// Anything else is safe to return.
|
||||
return data;
|
||||
}
|
||||
exports.decode = decode;
|
||||
/** @internal */
|
||||
function unsafeDecodeToken(token) {
|
||||
if (!JWT_REGEX.test(token)) {
|
||||
return {};
|
||||
}
|
||||
const components = token.split(".").map((s) => Buffer.from(s, "base64").toString());
|
||||
let payload = components[1];
|
||||
if (typeof payload === "string") {
|
||||
try {
|
||||
const obj = JSON.parse(payload);
|
||||
if (typeof obj === "object") {
|
||||
payload = obj;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
// ignore error
|
||||
}
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
exports.unsafeDecodeToken = unsafeDecodeToken;
|
||||
/**
|
||||
* Decode, but not verify, a Auth ID token.
|
||||
*
|
||||
* Do not use in production. Token should always be verified using the Admin SDK.
|
||||
*
|
||||
* This is exposed only for testing.
|
||||
*/
|
||||
/** @internal */
|
||||
function unsafeDecodeIdToken(token) {
|
||||
const decoded = unsafeDecodeToken(token);
|
||||
decoded.uid = decoded.sub;
|
||||
return decoded;
|
||||
}
|
||||
exports.unsafeDecodeIdToken = unsafeDecodeIdToken;
|
||||
/**
|
||||
* Decode, but not verify, an App Check token.
|
||||
*
|
||||
* Do not use in production. Token should always be verified using the Admin SDK.
|
||||
*
|
||||
* This is exposed only for testing.
|
||||
*/
|
||||
/** @internal */
|
||||
function unsafeDecodeAppCheckToken(token) {
|
||||
const decoded = unsafeDecodeToken(token);
|
||||
decoded.app_id = decoded.sub;
|
||||
return decoded;
|
||||
}
|
||||
exports.unsafeDecodeAppCheckToken = unsafeDecodeAppCheckToken;
|
||||
/**
|
||||
* Check and verify tokens included in the requests. Once verified, tokens
|
||||
* are injected into the callable context.
|
||||
*
|
||||
* @param {Request} req - Request sent to the Callable function.
|
||||
* @param {CallableContext} ctx - Context to be sent to callable function handler.
|
||||
* @returns {CallableTokenStatus} Status of the token verifications.
|
||||
*/
|
||||
/** @internal */
|
||||
async function checkTokens(req, ctx, options) {
|
||||
const verifications = {
|
||||
app: "INVALID",
|
||||
auth: "INVALID",
|
||||
};
|
||||
[verifications.auth, verifications.app] = await Promise.all([
|
||||
checkAuthToken(req, ctx),
|
||||
checkAppCheckToken(req, ctx, options),
|
||||
]);
|
||||
const logPayload = {
|
||||
verifications,
|
||||
"logging.googleapis.com/labels": {
|
||||
"firebase-log-type": "callable-request-verification",
|
||||
},
|
||||
};
|
||||
const errs = [];
|
||||
if (verifications.app === "INVALID") {
|
||||
errs.push("AppCheck token was rejected.");
|
||||
}
|
||||
if (verifications.auth === "INVALID") {
|
||||
errs.push("Auth token was rejected.");
|
||||
}
|
||||
if (errs.length === 0) {
|
||||
logger.debug("Callable request verification passed", logPayload);
|
||||
}
|
||||
else {
|
||||
logger.warn(`Callable request verification failed: ${errs.join(" ")}`, logPayload);
|
||||
}
|
||||
return verifications;
|
||||
}
|
||||
/** @interanl */
|
||||
async function checkAuthToken(req, ctx) {
|
||||
const authorization = req.header("Authorization");
|
||||
if (!authorization) {
|
||||
return "MISSING";
|
||||
}
|
||||
const match = authorization.match(/^Bearer (.*)$/i);
|
||||
if (!match) {
|
||||
return "INVALID";
|
||||
}
|
||||
const idToken = match[1];
|
||||
try {
|
||||
let authToken;
|
||||
if ((0, debug_1.isDebugFeatureEnabled)("skipTokenVerification")) {
|
||||
authToken = unsafeDecodeIdToken(idToken);
|
||||
}
|
||||
else {
|
||||
authToken = await (0, auth_1.getAuth)((0, app_1.getApp)()).verifyIdToken(idToken);
|
||||
}
|
||||
ctx.auth = {
|
||||
uid: authToken.uid,
|
||||
token: authToken,
|
||||
};
|
||||
return "VALID";
|
||||
}
|
||||
catch (err) {
|
||||
logger.warn("Failed to validate auth token.", err);
|
||||
return "INVALID";
|
||||
}
|
||||
}
|
||||
exports.checkAuthToken = checkAuthToken;
|
||||
/** @internal */
|
||||
async function checkAppCheckToken(req, ctx, options) {
|
||||
var _a;
|
||||
const appCheckToken = req.header("X-Firebase-AppCheck");
|
||||
if (!appCheckToken) {
|
||||
return "MISSING";
|
||||
}
|
||||
try {
|
||||
let appCheckData;
|
||||
if ((0, debug_1.isDebugFeatureEnabled)("skipTokenVerification")) {
|
||||
const decodedToken = unsafeDecodeAppCheckToken(appCheckToken);
|
||||
appCheckData = { appId: decodedToken.app_id, token: decodedToken };
|
||||
if (options.consumeAppCheckToken) {
|
||||
appCheckData.alreadyConsumed = false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
const appCheck = (0, app_check_1.getAppCheck)((0, app_1.getApp)());
|
||||
if (options.consumeAppCheckToken) {
|
||||
if (((_a = appCheck.verifyToken) === null || _a === void 0 ? void 0 : _a.length) === 1) {
|
||||
const errorMsg = "Unsupported version of the Admin SDK." +
|
||||
" App Check token will not be consumed." +
|
||||
" Please upgrade the firebase-admin to the latest version.";
|
||||
logger.error(errorMsg);
|
||||
throw new HttpsError("internal", "Internal Error");
|
||||
}
|
||||
appCheckData = await (0, app_check_1.getAppCheck)((0, app_1.getApp)()).verifyToken(appCheckToken, { consume: true });
|
||||
}
|
||||
else {
|
||||
appCheckData = await (0, app_check_1.getAppCheck)((0, app_1.getApp)()).verifyToken(appCheckToken);
|
||||
}
|
||||
}
|
||||
ctx.app = appCheckData;
|
||||
return "VALID";
|
||||
}
|
||||
catch (err) {
|
||||
logger.warn("Failed to validate AppCheck token.", err);
|
||||
if (err instanceof HttpsError) {
|
||||
throw err;
|
||||
}
|
||||
return "INVALID";
|
||||
}
|
||||
}
|
||||
/** @internal */
|
||||
function onCallHandler(options, handler, version) {
|
||||
const wrapped = wrapOnCallHandler(options, handler, version);
|
||||
return (req, res) => {
|
||||
return new Promise((resolve) => {
|
||||
res.on("finish", resolve);
|
||||
cors(options.cors)(req, res, () => {
|
||||
resolve(wrapped(req, res));
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
exports.onCallHandler = onCallHandler;
|
||||
function encodeSSE(data) {
|
||||
return `data: ${JSON.stringify(data)}\n\n`;
|
||||
}
|
||||
/** @internal */
|
||||
function wrapOnCallHandler(options, handler, version) {
|
||||
return async (req, res) => {
|
||||
var _a;
|
||||
const abortController = new AbortController();
|
||||
let heartbeatInterval = null;
|
||||
const heartbeatSeconds = options.heartbeatSeconds === undefined ? exports.DEFAULT_HEARTBEAT_SECONDS : options.heartbeatSeconds;
|
||||
const clearScheduledHeartbeat = () => {
|
||||
if (heartbeatInterval) {
|
||||
clearTimeout(heartbeatInterval);
|
||||
heartbeatInterval = null;
|
||||
}
|
||||
};
|
||||
const scheduleHeartbeat = () => {
|
||||
clearScheduledHeartbeat();
|
||||
if (!abortController.signal.aborted) {
|
||||
heartbeatInterval = setTimeout(() => {
|
||||
if (!abortController.signal.aborted) {
|
||||
res.write(": ping\n\n");
|
||||
scheduleHeartbeat();
|
||||
}
|
||||
}, heartbeatSeconds * 1000);
|
||||
}
|
||||
};
|
||||
res.on("close", () => {
|
||||
clearScheduledHeartbeat();
|
||||
abortController.abort();
|
||||
});
|
||||
try {
|
||||
if (!isValidRequest(req)) {
|
||||
logger.error("Invalid request, unable to process.");
|
||||
throw new HttpsError("invalid-argument", "Bad Request");
|
||||
}
|
||||
const context = { rawRequest: req };
|
||||
// TODO(colerogers): yank this when we release a breaking change of the CLI that removes
|
||||
// our monkey-patching code referenced below and increases the minimum supported SDK version.
|
||||
//
|
||||
// Note: This code is needed to fix v1 callable functions in the emulator with a monorepo setup.
|
||||
// The original monkey-patched code lived in the functionsEmulatorRuntime
|
||||
// (link: https://github.com/firebase/firebase-tools/blob/accea7abda3cc9fa6bb91368e4895faf95281c60/src/emulator/functionsEmulatorRuntime.ts#L480)
|
||||
// and was not compatible with how monorepos separate out packages (see https://github.com/firebase/firebase-tools/issues/5210).
|
||||
if ((0, debug_1.isDebugFeatureEnabled)("skipTokenVerification") && version === "gcfv1") {
|
||||
const authContext = context.rawRequest.header(exports.CALLABLE_AUTH_HEADER);
|
||||
if (authContext) {
|
||||
logger.debug("Callable functions auth override", {
|
||||
key: exports.CALLABLE_AUTH_HEADER,
|
||||
value: authContext,
|
||||
});
|
||||
context.auth = JSON.parse(decodeURIComponent(authContext));
|
||||
delete context.rawRequest.headers[exports.CALLABLE_AUTH_HEADER];
|
||||
}
|
||||
const originalAuth = context.rawRequest.header(exports.ORIGINAL_AUTH_HEADER);
|
||||
if (originalAuth) {
|
||||
context.rawRequest.headers["authorization"] = originalAuth;
|
||||
delete context.rawRequest.headers[exports.ORIGINAL_AUTH_HEADER];
|
||||
}
|
||||
}
|
||||
const tokenStatus = await checkTokens(req, context, options);
|
||||
if (tokenStatus.auth === "INVALID") {
|
||||
throw new HttpsError("unauthenticated", "Unauthenticated");
|
||||
}
|
||||
if (tokenStatus.app === "INVALID") {
|
||||
if (options.enforceAppCheck) {
|
||||
throw new HttpsError("unauthenticated", "Unauthenticated");
|
||||
}
|
||||
else {
|
||||
logger.warn("Allowing request with invalid AppCheck token because enforcement is disabled");
|
||||
}
|
||||
}
|
||||
if (tokenStatus.app === "MISSING" && options.enforceAppCheck) {
|
||||
throw new HttpsError("unauthenticated", "Unauthenticated");
|
||||
}
|
||||
const instanceId = req.header("Firebase-Instance-ID-Token");
|
||||
if (instanceId) {
|
||||
// Validating the token requires an http request, so we don't do it.
|
||||
// If the user wants to use it for something, it will be validated then.
|
||||
// Currently, the only real use case for this token is for sending
|
||||
// pushes with FCM. In that case, the FCM APIs will validate the token.
|
||||
context.instanceIdToken = req.header("Firebase-Instance-ID-Token");
|
||||
}
|
||||
const acceptsStreaming = req.header("accept") === "text/event-stream";
|
||||
if (acceptsStreaming && version === "gcfv1") {
|
||||
// streaming responses are not supported in v1 callable
|
||||
throw new HttpsError("invalid-argument", "Unsupported Accept header 'text/event-stream'");
|
||||
}
|
||||
const data = decode(req.body.data);
|
||||
if (options.authPolicy) {
|
||||
const authorized = await options.authPolicy((_a = context.auth) !== null && _a !== void 0 ? _a : null, data);
|
||||
if (!authorized) {
|
||||
throw new HttpsError("permission-denied", "Permission Denied");
|
||||
}
|
||||
}
|
||||
let result;
|
||||
if (version === "gcfv1") {
|
||||
result = await handler(data, context);
|
||||
}
|
||||
else {
|
||||
const arg = {
|
||||
...context,
|
||||
data,
|
||||
acceptsStreaming,
|
||||
};
|
||||
const responseProxy = {
|
||||
sendChunk(chunk) {
|
||||
// if client doesn't accept sse-protocol, response.write() is no-op.
|
||||
if (!acceptsStreaming) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
// if connection is already closed, response.write() is no-op.
|
||||
if (abortController.signal.aborted) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
const formattedData = encodeSSE({ message: chunk });
|
||||
let resolve;
|
||||
let reject;
|
||||
const p = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
const wrote = res.write(formattedData, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(wrote);
|
||||
});
|
||||
// Reset heartbeat timer after successful write
|
||||
if (wrote && heartbeatInterval !== null && heartbeatSeconds > 0) {
|
||||
scheduleHeartbeat();
|
||||
}
|
||||
return p;
|
||||
},
|
||||
signal: abortController.signal,
|
||||
};
|
||||
if (acceptsStreaming) {
|
||||
// SSE always responds with 200
|
||||
res.status(200);
|
||||
if (heartbeatSeconds !== null && heartbeatSeconds > 0) {
|
||||
scheduleHeartbeat();
|
||||
}
|
||||
}
|
||||
// For some reason the type system isn't picking up that the handler
|
||||
// is a one argument function.
|
||||
result = await handler(arg, responseProxy);
|
||||
clearScheduledHeartbeat();
|
||||
}
|
||||
if (!abortController.signal.aborted) {
|
||||
// Encode the result as JSON to preserve types like Dates.
|
||||
result = encode(result);
|
||||
// If there was some result, encode it in the body.
|
||||
const responseBody = { result };
|
||||
if (acceptsStreaming) {
|
||||
res.write(encodeSSE(responseBody));
|
||||
res.end();
|
||||
}
|
||||
else {
|
||||
res.status(200).send(responseBody);
|
||||
}
|
||||
}
|
||||
else {
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
if (!abortController.signal.aborted) {
|
||||
let httpErr = err;
|
||||
if (!(err instanceof HttpsError)) {
|
||||
// This doesn't count as an 'explicit' error.
|
||||
logger.error("Unhandled error", err);
|
||||
httpErr = new HttpsError("internal", "INTERNAL");
|
||||
}
|
||||
const { status } = httpErr.httpErrorCode;
|
||||
const body = { error: httpErr.toJSON() };
|
||||
if (version === "gcfv2" && req.header("accept") === "text/event-stream") {
|
||||
res.write(encodeSSE(body));
|
||||
res.end();
|
||||
}
|
||||
else {
|
||||
res.status(status).send(body);
|
||||
}
|
||||
}
|
||||
else {
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
clearScheduledHeartbeat();
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user