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

部署在Google Cloud Run的Node.js应用调用Square API遇问题求助

问题描述

在使用Square API创建客户时,前端部署在https://buzzysweets.com,后端部署在Google Cloud Run,发起POST请求时出现两个核心错误:

  • 504网关超时:向https://backend-437525971388.us-central1.run.app/customer的POST请求返回504错误
  • CORS错误:跨域请求被拦截,目标资源缺少Access-Control-Allow-Origin响应头

已验证信息

  • 生产环境的应用ID、位置ID、访问令牌、CDN配置均确认正确
  • 开发环境表现:
    • 沙盒凭证+CDN:可正常创建客户、订单、支付
    • 生产凭证+CDN:可正常创建客户、订单

代码片段

前端(React)代码

import React, { useEffect, useState } from 'react';
import { selectCart } from '../../features/cart/cartSlice';
import { useSelector } from 'react-redux';

const PaymentForm = () => {
  const appId = process.env.REACT_APP_YOUR_SQUARE_APPLICATION_ID;
  const locationId = process.env.REACT_APP_YOUR_SQUARE_LOCATION_ID;

  const { customerInfo } = useSelector(selectCart);
  const [card, setCard] = useState(null);

  // Load Square Web Payments SDK
  useEffect(() => {
    const loadSquare = async () => {
      if (!window.Square) {
        return;
      }

      const payments = window.Square.payments(appId, locationId);

      try {
        const card = await payments.card();
        await card.attach('#card-container');
        setCard(card);
      } catch (err) {
        console.error('Error loading Square SDK', err);
      }
    };

    loadSquare();
  }, []);

  const handlePayment = async (e) => {
    e.preventDefault();
    if (!card) return;

    try {
      const tokenResult = await card.tokenize();
      if (tokenResult.status === 'OK') {
        const customerResults = await createCustomer(tokenResult.token, customerInfo);
      }
    } catch (err) {
      console.error('Payment error:', err);
    }
  };

  const createCustomer = async (token, customerInfo) => {
    const bodyParameters = {
      address: {
        address: customerInfo.address,
        country: customerInfo.country,
        firstName: customerInfo.firstName,
        lastName: customerInfo.lastName,
        zipCode: customerInfo.zipcode,
      },
      givenName: customerInfo.firstName,
      familyName: customerInfo.lastName,
      emailAddress: customerInfo.emailAddress,
      idempotency_key: window.crypto.randomUUID(),
    };

    const body = JSON.stringify(bodyParameters);
    
    try {
      const response = await fetch(`${process.env.REACT_APP_API_URL_BACK}/customer`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${token}`,
        },
        body,
      });

      const result = await response.json();

      if (!response.ok) {
        throw new Error(`Failed to create customer: ${result.errors[0].detail}`);
      }

      return result;
    } catch (err) {
      console.error('Error creating customer:', err);
    }
  };
};

后端(Node.js)代码

server.js

const { ApiError, client: square } = require('./server/square');
const { json, send, router, post } = require('micro');
const corsMiddleware = require('micro-cors')({
  origin: process.env.REACT_APP_API_URL_FRONT,
  allowMethods: ['POST', 'GET'],
  allowedHeaders: ['Content-Type', 'Authorization'],
});
const retry = require('p-retry');
const logger = console;

async function createCustomer(req, res) {
  const payload = await json(req);

  await retry(async (bail, attempt) => {
    try {
      const customerReq = {
        address: {
          addressLine1: payload.address.address,
          firstName: payload.address.firstName,
          lastName: payload.address.lastName,
          country: payload.address.country,
          postalCode: payload.address.zipCode,
        },
        emailAddress: payload.emailAddress,
        idempotencyKey: payload.idempotency_key,
        familyName: payload.familyName,
      };

       console.log('customerReq:', customerReq) // 此日志在Cloud Run中正常输出
     const { result, statusCode } = await square.customersApi.createCustomer(customerReq);
      console.log('customer returned result:', result); // Cloud Run中无此日志输出
      
send(res, statusCode, {
        success: true,
        customer: {
          id: result.customer.id,
          status: result.customer.status,
        },
      });
    } catch (ex) {
      if (ex instanceof ApiError) {
        logger.error("API Error:", ex.errors);
        bail(ex);
      } else {
        logger.error(`Unexpected error: ${ex}`);
        throw ex;
      }
    }
  });
}

const appRouter = router(post('/customer', createCustomer));
const corsWrappedApp = corsMiddleware(appRouter);

if (require.main === module) {
  const PORT = process.env.PORT || 8080;
  require('http')
    .createServer(corsWrappedApp)
    .listen(PORT);
}

module.exports = corsWrappedApp;

config.js

require('dotenv').config({
  path: process.env.NODE_ENV === 'production' ? '.env.production' : '.env.sandbox',
});

const { Client, Environment } = require('square');

const client = new Client({
  environment: process.env.NODE_ENV === 'production' ? Environment.Production : Environment.Sandbox,
  accessToken: process.env.SQUARE_ACCESS_TOKEN, 
});

module.exports = {
  client,
  isProduction: process.env.NODE_ENV === 'production',
};

square.js

const { client } = require('./config');
const { ApiError } = require('square');

(async () => {
  try {
    const { result, statusCode } = await client.customersApi.listCustomers();
    console.log('Customers retrieved successfully:', result.customers);
  } catch (ex) {
    if (ex instanceof ApiError) {
      console.error('API Error:', ex.errors);
    } else {
       console.error('Unexpected Error:', ex);
    }
  }
})();

module.exports = { ApiError: client.ApiError, client };

错误分析与修复方案

1. CORS错误修复

从后端配置看,micro-cors中间件的配置存在潜在问题:

  • 环境变量未正确加载:Google Cloud Run生产环境中可能未正确设置REACT_APP_API_URL_FRONT,导致CORS的origin规则失效
  • 预检请求未处理:浏览器跨域POST前会发送OPTIONS预检请求,当前配置未允许该方法

修复步骤:

  • 登录Google Cloud Run控制台,确认服务的环境变量REACT_APP_API_URL_FRONT已设置为https://buzzysweets.com(不要带末尾斜杠)
  • 修改micro-cors配置,添加OPTIONS方法支持:
const corsMiddleware = require('micro-cors')({
  origin: process.env.REACT_APP_API_URL_FRONT,
  allowMethods: ['POST', 'GET', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization'],
});
  • 用curl验证预检请求响应:
curl -X OPTIONS https://backend-437525971388.us-central1.run.app/customer -H "Origin: https://buzzysweets.com" -v

确保响应头包含Access-Control-Allow-Origin: https://buzzysweets.com

2. 504网关超时修复

Cloud Run日志显示请求卡在square.customersApi.createCustomer(customerReq)步骤,核心原因可能是:

  • Square API权限/参数错误:生产环境下的请求参数格式不符合要求,或Access Token缺少CUSTOMERS_WRITE权限
  • 请求超时配置过短:Cloud Run默认超时时间(5秒)不足以等待Square API响应,或Square客户端未设置超时
  • 前端Authorization头误用:前端传递的卡片令牌是用于支付的,不是用于调用Square客户API的凭证

修复步骤:

(1)修正Square API调用凭证

前端传递的Bearer ${token}是Square卡片令牌,仅用于支付创建,不能用于客户创建。后端调用Square客户API时,应使用自身配置的Access Token,无需依赖前端的Authorization头:

  • 前端createCustomer请求中移除Authorization头:
const response = await fetch(`${process.env.REACT_APP_API_URL_BACK}/customer`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    // 移除Authorization头
  },
  body,
});

(2)调整超时配置

  • 在Google Cloud Run控制台,将服务的超时时间调整为30秒
  • 在Square客户端配置中添加超时限制:
// config.js
const client = new Client({
  environment: process.env.NODE_ENV === 'production' ? Environment.Production : Environment.Sandbox,
  accessToken: process.env.SQUARE_ACCESS_TOKEN,
  timeout: 20000, // 设置20秒超时
});

(3)验证权限与参数

  • 在Square开发者控制台,确认生产环境的Access Token拥有CUSTOMERS_WRITE权限
  • 检查customerReq中的地址字段:生产环境要求addressLine1非空,postalCode符合对应国家格式规范
  • 完善错误日志,打印Square API返回的完整错误信息:
catch (ex) {
  if (ex instanceof ApiError) {
    console.error("Square API Error:", JSON.stringify(ex.errors));
    bail(ex);
  } else {
    console.error(`Unexpected error: ${ex.message}`, ex.stack);
    throw ex;
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 15:57:03