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

React应用获取Cloud Storage签名URL遇CORS错误,本地正常线上异常

问题:生产环境下/signedUrl接口请求超时并返回CORS错误

我的React应用向服务器的/signedUrl接口发送POST请求,生成Firebase Storage Bucket的短期签名下载URL。服务器已配置允许任意源请求,但该接口会挂起10分钟后返回500错误,控制台输出:

Access to XMLHttpRequest at 'https://myserver.com/signedUrl' from origin 'https://myclient.web.app' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

本地环境功能正常,但部署后的开发/生产环境失败。storageService.ts中的moveFile()、deleteFile()、deleteDirectory()等其他函数均能正常工作;当createTemporaryDownloadUrl()直接返回假URL而不执行存储操作时,接口也能正常响应。已排除process.env.PROJECT_ID获取失败的可能(其他服务函数依赖该变量且运行正常)。


服务器端代码

App.ts

const app = express()
  .use(cors({ origin: true }))
  .use(express.json())
  .use(express.urlencoded())
  .use("/signedUrl", signedUrlRouter)

SignedUrlController.ts

export const signedUrlRouter = express.Router();

signedUrlRouter.post("/", async (request: Request, response: Response): Promise<void> => {

  let fileIdWithPath: string = request.body?.fileIdWithPath || "";

  if (fileIdWithPath === "") throw new Error("Must provide a fileIdWithPath");

  const url = await createTemporaryDownloadUrl(Buckets.ORG, `${fileIdWithPath}`);
  response.status(200).send({ url });
});

StorageService.ts

import { Storage, GetSignedUrlConfig } from "@google-cloud/storage";

export enum Buckets {
  ORG = ".appspot.com",
  // ... other buckets
}

const storage = new Storage();

const getBucketName = (bucket: Buckets): string => {
  return `${process.env.PROJECT_ID}${bucket}`;
};

export const createTemporaryDownloadUrl = async (bucket: Buckets, fileIdWithPath: string): Promise<string> => {
  const options = {
    version: "v4",
    action: "read",
    expires: Date.now() + 10 * 1000, // 10sec
  } as GetSignedUrlConfig;

  const [url] = await storage.bucket(getBucketName(bucket)).file(fileIdWithPath).getSignedUrl(options);
  return url;
};

Firebase配置

存储桶CORS配置

[
  {
    "origin": ["*"],
    "responseHeader": ["*"],
    "method": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    "maxAgeSeconds": 3600
  }
]

存储桶安全规则

service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read: if request.auth != null;
      allow write: if false;
    }
  }
}

请求/响应详情

OPTIONS预检查请求

通用头

Request URL: https://myserver.com/signedUrl
Request Method: OPTIONS
Status Code: 204 No Content
Remote Address: ***.***.***.***:***
Referrer Policy: strict-origin-when-cross-origin

请求头

:authority: dobee-dev.ew.r.appspot.com
:method: OPTIONS
:path: /signedUrl
:scheme: https
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,bn;q=0.8
Access-Control-Request-Headers: authorization,content-type,organizationid
Access-Control-Request-Method: POST
Origin: https://myclient.web.app
Referer: https://myclient.web.app/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Mobile Safari/537.36 Edg/119.0.0.0

响应头

Access-Control-Allow-Headers: authorization,content-type,organizationid
Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE
Access-Control-Allow-Origin: https://myclient.web.app
Alt-Svc: h3=":---"; ma=---,h3-29=":---"; ma=---
Content-Length: 0
Content-Type: text/html
Date: Fri, 17 Nov 2023 05:24:43 GMT
Server: Google Frontend
Vary: Origin, Access-Control-Request-Headers
X-Cloud-Trace-Context: ----------
X-Powered-By: Express

POST请求

通用头

Request URL: https://myserver.com/signedUrl
Request Method: POST
Status Code: 500 Internal Server Error
Referrer Policy: strict-origin-when-cross-origin

请求头

:authority: myserver.com
:method: POST
:path: /signedUrl
:scheme: https
Accept: application/json, text/plain, */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,bn;q=0.8
Authorization: Bearer <token>
Content-Length: 89
Content-Type: application/json
Dnt: 1
Organizationid: <orgID>
Origin: https://myclient.web.app
Referer: https://myclient.web.app/
Sec-Ch-Ua: "Microsoft Edge";v="119", "Chromium";v="119", "Not?A_Brand";v="24"
Sec-Ch-Ua-Mobile: ?1
Sec-Ch-Ua-Platform: "Android"
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Mobile Safari/537.36 Edg/119.0.0.0

排查与解决方案

  1. 添加异常处理确保CORS头始终存在
    当前控制器未处理createTemporaryDownloadUrl抛出的异常,导致超时返回500错误时未携带CORS头,浏览器误判为CORS问题。修改控制器代码:

    signedUrlRouter.post("/", async (request: Request, response: Response): Promise<void> => {
      try {
        let fileIdWithPath: string = request.body?.fileIdWithPath || "";
        if (fileIdWithPath === "") throw new Error("Must provide a fileIdWithPath");
        const url = await createTemporaryDownloadUrl(Buckets.ORG, `${fileIdWithPath}`);
        response.status(200).send({ url });
      } catch (err) {
        response.status(500).send({ error: err instanceof Error ? err.message : "Unknown error" });
      }
    });
    
  2. 验证服务器身份凭证权限
    部署环境中@google-cloud/storage可能缺少权限:

    • 确认服务器部署时已配置GOOGLE_APPLICATION_CREDENTIALS环境变量
    • 为默认应用引擎服务账号赋予Storage Object Viewer权限
  3. 定位签名URL生成超时原因

    • 检查生产环境中fileIdWithPath对应的文件是否存在,路径是否区分大小写
    • 查看服务器日志,获取500错误的具体栈信息,确认getSignedUrl调用失败的根源

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 02:35:07