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

Stripe Webhook调用失败排查:Next.js API端点出现504 FUNCTION_INVOCATION_TIMEOUT错误

Fixing Stripe Webhook 504 FUNCTION_INVOCATION_TIMEOUT in Next.js

Let's break down why you're hitting this timeout issue, even though your business logic (creating orders and updating ads) works correctly:

Core Issues

  1. You're not sending a complete response to Stripe
    In your current code, when handling checkout.session.completed, you have then(() => res.status(200))—this only sets the status code but doesn't actually send the response back to Stripe. Stripe waits for a full HTTP response until it hits the timeout limit.

  2. DynamoDB operations use callbacks instead of async/await
    Your createOrder function uses callback-based DynamoDB methods, which means the function resolves immediately (before the DB operations finish) but your response logic still doesn't send a proper reply.

  3. Next.js API route requires explicit response termination
    Next.js API routes won't end automatically—you have to explicitly send a response (like .json() or .end()) or the function will run until it times out.

Step-by-Step Fix

1. Prioritize sending a fast response to Stripe

Stripe recommends responding to webhooks within 10 seconds, and you don't need to wait for business logic to finish. Send the success response first, then handle the DB work asynchronously.

2. Rewrite DynamoDB operations to use async/await

Callback-based code is harder to reason about and can lead to unexpected timing issues. Switch to using .promise() with async/await for cleaner, more reliable code.

3. Ensure proper response flow

Make sure you send the response before running long-running tasks, and explicitly return after sending to avoid extra code execution.

Here's the revised code:

import { buffer } from "micro"
const AWS = require("aws-sdk")
AWS.config.update({
  accessKeyId: process.env.MY_AWS_ACCESS_KEY_ID,
  secretAccessKey: process.env.MY_AWS_SECRET_ACCESS_KEY,
  region: process.env.MY_AWS_REGION,
  endpoint: process.env.MY_AWS_ENDPOINT,
})
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY)
const endpointSecret = process.env.STRIPE_CREATE_ORDER_SIGNING_SECRET

// Rewrite createOrder with async/await for DynamoDB
const createOrder = async (session) => {
  console.log("create order - session.id: ", session.id)
  console.log("create order - session.metadata.userID: ", session.metadata.userID)
  console.log("create order - session.amount_total / 100: ", session.amount_total / 100)
  
  const docClient = new AWS.DynamoDB.DocumentClient()
  const date = new Date()

  // Create order with await
  const orderParams = {
    TableName: process.env.ORDER_TABLE_NAME,
    Item: {
      id: session.id,
      userID: session.metadata.userID,
      amount: session.amount_total / 100,
      adID: session.metadata.adID,
      createdAt: date.toISOString(),
      updatedAt: date.toISOString(),
    },
  }
  try {
    const orderData = await docClient.put(orderParams).promise()
    console.log("Order put Success - " + JSON.stringify(orderData, null, 2))
  } catch (err) {
    console.log("Order put err - " + JSON.stringify(err, null, 2))
  }

  // Calculate TTL and update ad
  const ninetyDays = 1000 * 60 * 60 * 24 * 90
  const currTime = Date.now()
  const ttlSeconds = Math.ceil((ninetyDays + currTime) / 1000)

  const adParams = {
    TableName: process.env.AD_TABLE_NAME,
    Key: { id: session.metadata.adID },
    UpdateExpression: "set paid = :paid, expdate = :expdate",
    ExpressionAttributeValues: {
      ":paid": true,
      ":expdate": ttlSeconds,
    },
    ReturnValues: "UPDATED_NEW",
  }
  try {
    const adData = await docClient.update(adParams).promise()
    console.log("UPDATE Ad Success - " + JSON.stringify(adData, null, 2))
  } catch (err) {
    console.log("UPDATE Ad err - " + JSON.stringify(err, null, 2))
  }
}

export default async (req, res) => {
  if (req.method === "POST") {
    const requestBuffer = await buffer(req)
    const payload = requestBuffer.toString()
    const sig = req.headers["stripe-signature"]
    let event

    // Verify Stripe webhook signature
    try {
      event = stripe.webhooks.constructEvent(payload, sig, endpointSecret)
    } catch (err) {
      console.log("ERROR", err.message)
      return res.status(400).send(`Webhook create Order error: ${err.message}`)
    }

    // Handle checkout.session.completed event
    if (event.type === "checkout.session.completed") {
      const session = event.data.object
      
      // 1. Send success response to Stripe FIRST to avoid timeout
      res.status(200).json({ received: true })
      
      // 2. Run business logic asynchronously (doesn't block the response)
      try {
        await createOrder(session)
      } catch (err) {
        console.error(`Create Order Error - ${err.message}`)
        // Add error alerting here if needed (no need to reply to Stripe again)
      }
      
      // Exit early to prevent extra code from running
      return
    }

    // Default response for other event types
    res.status(200).json({ received: true })
  } else {
    // Handle non-POST requests
    res.setHeader("Allow", "POST")
    res.status(405).end("Method Not Allowed")
  }
}

export const config = {
  api: {
    bodyParser: false,
    externalResolver: true,
  },
}

Key Changes Explained

  • Fast response first: By sending res.status(200).json({ received: true }) before running createOrder, you ensure Stripe gets a reply immediately, avoiding the timeout.
  • Async/await for DynamoDB: Using .promise() with await makes the code easier to debug and ensures you properly catch errors in DB operations.
  • Explicit return: After sending the response, we return early to prevent any unintended code execution that could delay the response.

Extra Tips

  • Log response timing: Add a log when you send the response to confirm it's being sent quickly.
  • Use async task queues for long tasks: If your business logic ever takes longer than a few seconds, use a queue like AWS SQS + Lambda to handle it in the background.
  • Test with Stripe CLI: Use the Stripe CLI to test your webhook locally and simulate slow responses to verify your fix works.

内容的提问来源于stack exchange,提问作者Janis Basis Basovs

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.30 17:32:28