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
相关产品推荐
相关产品推荐

