You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Firebase模拟器运行图片缩放扩展报仅支持HTTP(S)协议错误

问题背景

我完整拷贝了Firebase Functions的Resize Image扩展包含依赖的所有代码,计划在Firebase Emulator中运行并调整适配自身需求,当前配置几乎与扩展源代码完全一致。

本地环境配置

  • node: v14.17.5
  • npm: 7.23.0
  • firebase: 9.20.0
  • firebase-admin: ^9.11.1
  • firebase-functions: ^3.15.6
  • mkdirp: ^1.0.4
  • uuid: ^8.3.2

扩展原生依赖版本

  • firebase-admin: ^8.0.0
  • firebase-functions: ^3.13.2
  • mkdirp: ^1.0.4
  • sharp: 0.23.4
  • uuidv4: ^6.1.0

核心代码与报错位置

报错行为:
await remoteFile.download({ destination: originalFile });

完整核心代码:

const resizeImage = async (object): Promise<ResizedImageResult[]> => {
  logs.start();
  const { contentType } = object; // This is the image MIME type

  const tmpFilePath = path.resolve('/', path.dirname(object.name)); // Absolute path to dirname

  if (!contentType) {
    logs.noContentType();
    return;
  }

  if (!contentType.startsWith('image/')) {
    logs.contentTypeInvalid(contentType);
    return;
  }

  if (object.contentEncoding === 'gzip') {
    logs.gzipContentEncoding();
    return;
  }

  if (!supportedContentTypes.includes(contentType)) {
    logs.unsupportedType(supportedContentTypes, contentType);
    return;
  }

  if (config.includePathList && !startsWithArray(config.includePathList, tmpFilePath)) {
    logs.imageOutsideOfPaths(config.includePathList, tmpFilePath);
    return;
  }

  if (config.excludePathList && startsWithArray(config.excludePathList, tmpFilePath)) {
    logs.imageInsideOfExcludedPaths(config.excludePathList, tmpFilePath);
    return;
  }

  if (object.metadata && object.metadata.resizedImage === 'true') {
    logs.imageAlreadyResized();
    return;
  }

  const bucket = admin.storage().bucket(object.bucket);
  const filePath = object.name; // File path in the bucket.
  const fileDir = path.dirname(filePath);
  const fileExtension = path.extname(filePath);
  const fileNameWithoutExtension = extractFileNameWithoutExtension(filePath, fileExtension);
  const objectMetadata = object;

  let originalFile;
  let remoteFile: File;
  try {
    originalFile = path.join(os.tmpdir(), filePath);
    const tempLocalDir = path.dirname(originalFile);

    // Create the temp directory where the storage file will be downloaded.
    logs.tempDirectoryCreating(tempLocalDir);
    await mkdirp(tempLocalDir);
    logs.tempDirectoryCreated(tempLocalDir);

    // Download file from bucket.
    remoteFile = bucket.file(filePath);
    logs.imageDownloading(filePath);
    logs.imageDownloading(originalFile);
    await remoteFile.download({ destination: originalFile }); // <- 报错代码行
    logs.imageDownloaded(filePath, originalFile);

    // Get a unique list of image types
    const imageTypes = new Set(config.imageTypes);

    // Convert to a set to remove any duplicate sizes
    const imageSizes = new Set(config.imageSizes);

    const tasks: Promise<ResizedImageResult>[] = [];

    imageTypes.forEach((format) => {
      imageSizes.forEach((size) => {
        tasks.push(
          modifyImage({
            bucket,
            originalFile,
            fileDir,
            fileNameWithoutExtension,
            fileExtension,
            contentType,
            size,
            objectMetadata: objectMetadata,
            format,
          }),
        );
      });
    });

    const results = await Promise.all(tasks);

    const failed = results.some((result) => result.success === false);
    if (failed) {
      logs.failed();
      return;
    } else {
      if (config.deleteOriginalFile === deleteImage.onSuccess) {
        if (remoteFile) {
          try {
            logs.remoteFileDeleting(filePath);
            await remoteFile.delete();
            logs.remoteFileDeleted(filePath);
          } catch (err) {
            logs.info('Catch 1');
            logs.errorDeleting(err);
          }
        }
      }
      logs.complete();
    }
  } catch (err) {
    logs.info('Catch 2');
    logs.error(err);
  } finally {
    if (originalFile) {
      logs.tempOriginalFileDeleting(filePath);
      fs.unlinkSync(originalFile);
      logs.tempOriginalFileDeleted(filePath);
    }
    if (config.deleteOriginalFile === deleteImage.always) {
      // Delete the original file
      if (remoteFile) {
        try {
          logs.remoteFileDeleting(filePath);
          await remoteFile.delete();
          logs.remoteFileDeleted(filePath);
        } catch (err) {
          logs.errorDeleting(err);
        }
      }
    }
  }
};

预期行为

执行await remoteFile.download({ destination: originalFile });时应正常下载原始文件,该代码与官方扩展源代码完全一致。

实际报错

{
  "severity":"ERROR","message":"Error when resizing image TypeError: Only HTTP(S) protocols are supported
    at getNodeRequestOptions (.../node_modules/teeny-request/node_modules/node-fetch/lib/index.js:1309:9)
    at .../node_modules/teeny-request/node_modules/node-fetch/lib/index.js:1410:19
    at new Promise (<anonymous>)
    at Function.fetch [as default] (.../node_modules/teeny-request/node_modules/node-fetch/lib/index.js:1407:9)
    at teenyRequest (.../node_modules/teeny-request/build/src/index.js:184:29)
    at Object.request (.../node_modules/teeny-request/build/src/index.js:241:20)
    at Timeout.makeRequest [as _onTimeout] (.../node_modules/retry-request/index.js:139:28)
    at listOnTimeout (internal/timers.js:557:17)
    at processTimers (internal/timers.js:500:7)"
}
解决方案

该报错是高版本firebase-admin与Firebase Emulator Storage的兼容性问题,高版本firebase-admin依赖的teeny-request库仅支持HTTP/HTTPS协议,而旧版Emulator返回的存储地址默认使用gs://协议,导致请求触发异常。无需降级Firebase核心版本,可通过以下任意一种方法解决:

  1. 显式指定Storage使用Emulator端点
    在firebase-admin初始化代码中,添加Emulator适配配置,官方提供的useEmulator方法会自动处理协议转换:
const admin = require('firebase-admin');
admin.initializeApp({
  projectId: "你的项目ID",
  storageBucket: "你的存储桶名称"
});

// 仅在本地Emulator环境下生效
if (process.env.FUNCTIONS_EMULATOR) {
  admin.storage().useEmulator("localhost", 9199);
}

注意9199是Firebase Storage Emulator的默认端口,如果你修改过端口,替换为对应端口即可。
2. 替换下载逻辑直接走HTTP请求
如果第一种方案不生效,可以临时修改下载逻辑,直接从Emulator的公开HTTP接口拉取文件:

// 替换原报错行代码
const publicUrl = `http://localhost:9199/storage/v1/b/${object.bucket}/o/${encodeURIComponent(filePath)}?alt=media`;
const res = await fetch(publicUrl);
const buffer = await res.buffer();
fs.writeFileSync(originalFile, buffer);
  1. 升级@google-cloud/storage依赖
    firebase-admin内置的@google-cloud/storage包旧版本存在协议适配bug,升级该依赖到5.8.5以上版本即可修复:
    执行命令:
    npm install @google-cloud/storage@latest

内容的提问来源于stack exchange,提问作者vdiaz1130

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.29 16:15:03