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

Firebase onRequest云函数配CORS后错误信息返回异常问题

编辑说明:遇到该问题的开发者请先了解onRequest与onCall云函数的差异,掌握二者在客户端的对应调用方式,可帮助理解本问题的产生根源。

问题描述
  • 技术栈:后端采用Firebase,前端采用React框架
  • 异常表现:需要实现云函数错误的正确返回,预期客户端展示错误提示The email address is already in use by another account;从请求响应可以看到返回体中确实包含该错误内容,但客户端执行console.log(err.message)时仅输出invalid-argument
  • 参考截图:请求信息
原有实现代码

云函数端 - index.ts

export const registerUser = https.onRequest((req, resp) => {
cors(req, resp, async () => {
    const data = req.body.data;
    const userData: IUserData = {
      firstName: data.firstName,
      lastName: data.lastName,
      statusId: 1,
      customerId: ''
    }
    
    try {
      // Create auth user in firebase
      const newAuthUser = await admin.auth().createUser({
        email: data.email,
        emailVerified: false,
        password: data.password,
        displayName: data.firstName + ' ' + data.lastName,
        disabled: false
      })
      // Create a stripe customer
      const uid = newAuthUser.uid;
      const stripeCustomer = await createStripeCustomer(data, uid);
      userData.customerId = stripeCustomer.id;
      // Create user doc in firestore
      const storeUserRef = app.firestore().doc('/users/' + uid);
      const storeUser = await storeUserRef.set(userData);
      resp.status(200).json({
        user: storeUser,
        message: "User successfully registered"
      }).send();
      return;
    } catch (err: any) {
      console.log(err);
      resp.status(400).send(err);
      return;
    }
  })
})

React端 - SignupPage.tsx

function SignUpPage() {
  const firebaseAuth = getAuth(FIREBASE_APP);
  const functions = getFunctions(FIREBASE_APP);
  const [auth, loading] = useAuthState(firebaseAuth);
  const [regError, setRegError] = useState("")
  const nav = useNavigate();
  const { register, handleSubmit, formState: { errors }, } = useForm();
  const [registering, setRegistering] = useState(false);
  const registerUser = httpsCallable(functions, 'registerUser');
  connectFunctionsEmulator(functions, "localhost", 5001);
  
  // Check if a user login exists
  useEffect(() => {
    // If the auth has finished loading and the user exists, redirect to the
    // user account page
    if (auth && !loading) { nav('/account') }
  }, [auth, loading, nav])

  /**
   * Form on submit
   * @param data 
   */
  const onSubmit = (data: any) => {
    setRegistering(true);
    setRegError("");
    const userData: IRegisterPayload = {
      firstName: data.firstName,
      lastName: data.lastName,
      email: data.email,
      password: data.password
    }
    registerUser(userData).then(async(res) => {
      // return await signInWithEmailAndPassword(
      //   firebaseAuth, userData.email, userData.password
      // );
      console.log('REGISTER USER SUCCESS: ', res)
    })
    .then(res => {
      console.log(res);
      nav("/account");
    })
    .catch((err) => {
      console.log('Console log err.message:', err.message);
      setRegError(err.message);
    }).finally(() => {
      setRegistering(false);
    })
  };

  return(
    <SiteLayout>
      <div className="px-3 py-5 mx-auto signup-wrapper w-100 container d-flex flex-column flex-grow-1">
        <div className="row">
          <div className="col-12 col-lg-6 col-xl-5 pe-lg-5">
            <h2 className="h2 mb-4 text-center">Sign up</h2>
            <p className="text-center">Already a member? <NavLink to={`/login`}>Login</NavLink></p>
            <form onSubmit={handleSubmit(onSubmit)}
              className="mb-2"
            >
              <div className="mb-3">
                <label>First Name</label>
                <input type={`text`}
                  className={`form-control${errors.firstName ? ' is-invalid': ''}`}
                  placeholder="Enter your first name"
                  disabled={registering}
                  {...register('firstName', { required: true})}
                />
                {errors.firstName && <small className="text-danger d-block mt-1">First Name is required</small>}
              </div>
              <div className="mb-3">
                <label>Last Name</label>
                <input type={`text`} 
                  className={`form-control${errors.lastName ? ' is-invalid': ''}`}
                  placeholder="Enter your last name"
                  disabled={registering}
                  {...register('lastName', { required: true})}
                />
                {errors.lastName && <small className="text-danger d-block mt-1">Last Name is required</small>}
              </div>
              <div className="mb-3">
                <label>Email</label>
                <input type={`email`} 
                  className={`form-control${errors.email ? ' is-invalid': ''}`}
                  placeholder="Enter your email address"
                  disabled={registering}
                  {...register('email', { required: true})}
                />
                {errors.email && <small className="text-danger d-block mt-1">Email is required</small>}
              </div>
              <div className="mb-4">
                <label>Password</label>
                <input type={`password`} 
                  className={`form-control${errors.password ? ' is-invalid': ''}`}
                  placeholder="Enter your password"
                  disabled={registering}
                  {...register('password',{ 
                    required: "You must specify a password",
                    validate: (val: string) => {
                      // Check the entered value meets the regex requirement
                      const isStrong = PASSWORD_REGEX.test(val);
                      return (val && isStrong) || "Password does not meet criteria";
                    }
                  })}
                />
                {errors.password && <p className="small text-danger d-block my-1">{errors.password.message}</p>}
                <small className="text-muted">Passwords must be a minimum of 8 characters and include:</small>
                <ul className="small text-muted">
                  <li>At least 1 uppercase letter</li>
                  <li>At least 1 lowercase letter</li>
                  <li>At least 1 special character</li>
                  <li>At least 1 number</li>
                </ul>
              </div>
              <Button isLoading={registering}
                isSubmit={true}
                loadingLabel="Registering..."
                className="w-100"
              >
                Sign up
              </Button>
              {regError &&
                <Message severity="error" text={regError} className="my-3 w-100" />
              }
            </form>
            <p className="small text-muted">
            By signing up, you confirm that you accept the Terms of Service and Privacy Policy. This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.
            </p>            
          </div>
        </div>
      </div>
    </SiteLayout>
  );
}
export default SignUpPage;
问题根因

核心问题是云函数定义方式与客户端调用方式不匹配:

  • 云函数端使用https.onRequest定义标准HTTP云函数,这类函数遵循普通HTTP接口规范,需要开发者自行处理CORS、参数解析、响应格式、错误序列化逻辑
  • 客户端使用httpsCallable发起调用,该方法是Firebase SDK专为https.onCall可调用云函数封装的调用入口,要求响应必须符合SDK指定的固定结构,不符合结构的返回会被SDK自动转换为通用错误,因此只能拿到默认的invalid-argument提示,无法获取自定义错误内容。
解决方案

二选一即可,优先推荐方案1。

方案1:统一使用onCall可调用云函数(官方推荐)

该方案不需要自行处理CORS、参数校验、响应格式封装等重复逻辑,SDK会自动完成序列化、鉴权等通用处理。

云函数端修改

将onRequest替换为onCall,错误抛出使用官方规范的HttpsError:

import { https } from 'firebase-functions';
// 其余原有依赖保持不变

export const registerUser = https.onCall(async (data, context) => {
  // onCall自动处理CORS,无需手动引入cors中间件
  const userData: IUserData = {
    firstName: data.firstName,
    lastName: data.lastName,
    statusId: 1,
    customerId: ''
  }
    
  try {
    const newAuthUser = await admin.auth().createUser({
      email: data.email,
      emailVerified: false,
      password: data.password,
      displayName: data.firstName + ' ' + data.lastName,
      disabled: false
    })
    const uid = newAuthUser.uid;
    const stripeCustomer = await createStripeCustomer(data, uid);
    userData.customerId = stripeCustomer.id;
    const storeUserRef = app.firestore().doc('/users/' + uid);
    await storeUserRef.set(userData);
    // 直接返回结果即可,SDK会自动包装为标准响应格式
    return {
      message: "User successfully registered"
    }
  } catch (err: any) {
    console.log(err);
    // 抛出规范的HttpsError,第一个参数为错误码,第二个为面向用户的错误提示
    throw new https.HttpsError(
      'already-exists',
      err.message
    );
  }
})

客户端修改

原有httpsCallable调用逻辑无需大改,捕获错误时直接读取err.message即可拿到自定义错误信息。

方案2:保留onRequest云函数,替换客户端调用方式

如果要保留HTTP类型云函数,客户端不要使用httpsCallable调用,改为普通HTTP请求方式自行处理响应。

云函数端修改

原有错误返回逻辑需要调整,Error对象直接序列化会丢失message字段,改为返回结构化JSON:

catch (err: any) {
  console.log(err);
  // 手动返回结构化错误信息
  resp.status(400).json({
    code: err.code,
    message: err.message
  });
  return;
}

客户端修改

使用fetch/axios等HTTP客户端直接调用云函数地址,示例:

const onSubmit = async (data: any) => {
  setRegistering(true);
  setRegError("");
  const userData: IRegisterPayload = {
    firstName: data.firstName,
    lastName: data.lastName,
    email: data.email,
    password: data.password
  }
  try {
    // 本地模拟器地址格式:http://localhost:5001/<项目ID>/<云函数区域>/registerUser
    // 生产环境替换为正式云函数触发地址
    const resp = await fetch("http://localhost:5001/替换为你的项目ID/us-central1/registerUser", {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify({data: userData})
    })
    const resData = await resp.json();
    if (!resp.ok) {
      throw new Error(resData.message);
    }
    console.log('REGISTER USER SUCCESS: ', resData)
    nav("/account");
  } catch (err: any) {
    console.log('Console log err.message:', err.message);
    setRegError(err.message);
  } finally {
    setRegistering(false);
  }
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 08:39:19