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核心版本,可通过以下任意一种方法解决:
- 显式指定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);
- 升级@google-cloud/storage依赖
firebase-admin内置的@google-cloud/storage包旧版本存在协议适配bug,升级该依赖到5.8.5以上版本即可修复:
执行命令:npm install @google-cloud/storage@latest
内容的提问来源于stack exchange,提问作者vdiaz1130
相关产品推荐
相关产品推荐

