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

Go语言Azure Function部署到Azure后执行失败,返回500内部服务器错误

Azure Function部署后返回500错误排查与修复

我用Go语言开发了一款Azure Function,功能为读取Azure存储账户中的内容并将数据发送至其他服务。该函数在本地环境运行正常,但部署到Azure后,调用时耗时较长且持续返回500内部服务器错误。相关代码及配置文件如下:

核心代码

package main

import (
	"bytes"
	"context"
	"encoding/csv"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"time"

	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
	"github.com/gin-gonic/gin"
	"github.com/joho/godotenv"
)

func handleError(err error) {
	if err != nil {
		log.Fatal(err.Error())
	}
}

func get_Port() string {
	port := ":8000"
	if val, ok := os.LookupEnv("FUNCTIONS_CUSTOMHANDLER_PORT"); ok {
		port = ":" + val
	}
	return port
}

func handleFunc() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Writer.Header().Set("Content-Type", "application/json")
		c.Writer.Header().Set("Access-Control-Allow-Origin", "https://portal.azure.com.")
		// 使用请求上下文替代Background,避免超时问题
		if err := postFunc(c.Request.Context()); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, gin.H{
			"body": "success",
		})
	}
}

func main() {
	// 仅在本地加载.env文件
	if os.Getenv("AZURE_FUNCTIONS_ENVIRONMENT") != "Production" {
		err := godotenv.Load()
		if err != nil {
			log.Println("Warning: 未加载.env文件")
		}
	}

	router := gin.Default()
	router.GET("/api/Ziyad", handleFunc())
	port := get_Port()
	router.Run(port)
}

// 修改为返回错误,避免直接终止进程
func postFunc(ctx context.Context) error {
	fmt.Printf("Azure Blob storage quick start sample\n")

	// 替换为实际存储账户名
	url := "https://{mystorageaccountname}.blob.core.windows.net/"

	credential, err := azidentity.NewDefaultAzureCredential(nil)
	if err != nil {
		log.Printf("获取Azure凭据失败: %v", err)
		return fmt.Errorf("获取Azure凭据失败: %w", err)
	}

	client, err := azblob.NewClient(url, credential, nil)
	if err != nil {
		log.Printf("创建Blob客户端失败: %v", err)
		return fmt.Errorf("创建Blob客户端失败: %w", err)
	}

	fmt.Println("列出容器中的Blob:")
	containerName := "bcfolder"
	blobName := "Book1.csv"

	blobDownloadResp, err := client.DownloadStream(ctx, containerName, blobName, nil)
	if err != nil {
		log.Printf("下载Blob流失败: %v", err)
		return fmt.Errorf("下载Blob流失败: %w", err)
	}
	defer blobDownloadResp.Body.Close()

	downloadedData, err := io.ReadAll(blobDownloadResp.Body)
	if err != nil {
		log.Printf("读取Blob数据失败: %v", err)
		return fmt.Errorf("读取Blob数据失败: %w", err)
	}

	csvData := csv.NewReader(bytes.NewBuffer(downloadedData))

	// 跳过首行
	_, err = csvData.Read()
	if err != nil {
		if err != io.EOF {
			log.Printf("读取CSV首行失败: %v", err)
			return fmt.Errorf("读取CSV首行失败: %w", err)
		}
	}

	for {
		line, err := csvData.Read()
		if err != nil {
			if err == io.EOF {
				break
			}
			log.Printf("读取CSV行失败: %v", err)
			return fmt.Errorf("读取CSV行失败: %w", err)
		}

		// 检查列数,避免索引越界
		if len(line) < 5 {
			log.Printf("CSV行数据不足5列: %v", line)
			continue
		}

		data := fmt.Sprintf(`{"jsontext":"{\"No\":\"%s\",\"desc\":\"%s\",\"UOM\":\"%s\",\"cost\":\"%s\",\"price\":\"%s\"}"}`,
			line[0], line[1], line[2], line[3], line[4])
		fmt.Printf("%s\n", data)
		payload := bytes.NewBuffer([]byte(data))

		if err := postToBC(ctx, payload); err != nil {
			log.Printf("发送数据到BC失败: %v", err)
			return fmt.Errorf("发送数据到BC失败: %w", err)
		}
		break
	}

	return nil
}

func get_access_token(ctx context.Context) (string, error) {
	client_Id := os.Getenv("client_id")
	client_Secret := os.Getenv("client_secret")
	tenantID := os.Getenv("tenantID")

	// 检查环境变量是否存在
	if client_Id == "" || client_Secret == "" || tenantID == "" {
		return "", fmt.Errorf("缺少必要的环境变量")
	}

	cred, err := confidential.NewCredFromSecret(client_Secret)
	if err != nil {
		log.Printf("创建凭据失败: %v", err)
		return "", fmt.Errorf("创建凭据失败: %w", err)
	}

	confidentialClient, err := confidential.New("https://login.microsoftonline.com/"+tenantID, client_Id, cred)
	if err != nil {
		log.Printf("创建机密客户端失败: %v", err)
		return "", fmt.Errorf("创建机密客户端失败: %w", err)
	}

	scopes := []string{"https://api.businesscentral.dynamics.com/.default"}
	result, err := confidentialClient.AcquireTokenSilent(ctx, scopes)
	if err != nil {
		result, err = confidentialClient.AcquireTokenByCredential(ctx, scopes)
		if err != nil {
			log.Printf("获取Token失败: %v", err)
			return "", fmt.Errorf("获取Token失败: %w", err)
		}
	}

	return result.AccessToken, nil
}

func postToBC(ctx context.Context, payload io.Reader) error {
	tenantID := os.Getenv("tenantID")
	if tenantID == "" {
		return fmt.Errorf("tenantID环境变量缺失")
	}

	url := "https://api.businesscentral.dynamics.com/v2.0/" + tenantID +
		"/sandbox2/ODataV4/MyWebService_InsertItemFromJson?company=Xeetek"
	method := "POST"

	// 设置HTTP超时
	client := &http.Client{Timeout: 10 * time.Second}
	req, err := http.NewRequestWithContext(ctx, method, url, payload)
	if err != nil {
		log.Printf("创建HTTP请求失败: %v", err)
		return fmt.Errorf("创建HTTP请求失败: %w", err)
	}

	req.Header.Add("Content-Type", "application/json")
	token, err := get_access_token(ctx)
	if err != nil {
		return fmt.Errorf("获取访问Token失败: %w", err)
	}
	req.Header.Add("Authorization", "Bearer "+token)

	res, err := client.Do(req)
	if err != nil {
		log.Printf("发送HTTP请求失败: %v", err)
		return fmt.Errorf("发送HTTP请求失败: %w", err)
	}
	defer res.Body.Close()

	if res.StatusCode >= 400 {
		body, _ := io.ReadAll(res.Body)
		log.Printf("BC返回错误状态码: %d, 响应内容: %s", res.StatusCode, string(body))
		return fmt.Errorf("BC返回错误: %d, %s", res.StatusCode, string(body))
	}

	body, err := io.ReadAll(res.Body)
	if err != nil {
		log.Printf("读取BC响应失败: %v", err)
		return fmt.Errorf("读取BC响应失败: %w", err)
	}
	fmt.Println(string(body))
	return nil
}

配置文件

local.settings.json

{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "custom",
    "AzureWebJobsStorage": "UseDevelopmentStorage=true"
  }
}

host.json

{
  "version": "2.0",
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[4.*, 5.0.0)"
  },
  "customHandler": {
    "enableForwardingHttpRequest": true,
    "description": {
      "defaultExecutablePath": "Muaaz.exe",
      "workingDirectory": "",
      "arguments": []
    }
  }
}

function.json

{
  "bindings": [
    {
      "authLevel": "Anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "get",
        "post"
      ],
      "route": "Ziyad"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ]
}

关键修复与排查步骤

  • 日志排查:登录Azure门户,进入Function App的「监控>日志」,查看具体错误栈,定位500错误的根源。
  • 环境变量配置:将client_id、client_secret、tenantID添加到Function App的「配置>应用程序设置」中,移除对本地.env文件的依赖。
  • 权限配置:
    • 给Function App的系统分配身份赋予存储账户的「存储Blob数据读取者」权限。
    • 确保服务主体(对应client_id)拥有Business Central API的访问权限。
  • 代码逻辑优化:
    • 替换log.Fatalln为错误返回,避免进程直接终止。
    • 使用请求上下文ctx替代context.Background(),适配Azure Function的生命周期。
    • 添加CSV列数检查,防止索引越界panic。
    • 给HTTP客户端设置超时,避免网络请求挂起。
  • Custom Handler验证:确保host.json中的defaultExecutablePath与部署后的可执行文件名一致。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 11:35:56