本文主要声讨的是华为 AppGallery 开发者文档。要值得称赞的是,他们还是写了为了这个功能编写了文档。如果你在写CI/CD的时候遇到这个问题,并通过该文章解决了,麻烦点个赞。
一般来说,你需要按照以下几步走。
第一步,获取华为应用市场 Access Token
首先,一开始,先看文档。获取服务端授权,然后把拿到的client_id 和 client_secret 进行如下请求。
const result = axios.post('https://connect-api.cloud.huawei.com/api/oauth2/v1/token', {
grant_type: 'client_credentials',
client_id: clientId, // 替换为你的 client_id
client_secret: clientSecret, // 替换为你的 client_secret
});
// 拿到的 Token
const accessToken = result.access_token;
第二步,申请上传权限
接着,直接看代码就行,到这一步还没有坑。
const contentLength = fs.statSync(apkFilePath).size;
const uploadApplyResult = axios.get(
"https://connect-api.cloud.huawei.com/api/publish/v2/upload-url/for-obs",
{
headers: {
client_id: clientId,
// 第一步拿到的 token
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
params: {
// 根据华为 API 要求构造请求体
// 1 代表 APK, 2 代表 AAB(Android App Bundle)
type: 1,
// 你在华为平台的应用 ID
appId: appId,
// 'apk' or 'aab'
suffix: 'apk',
// 上传的 APK 文件名
fileName: fileName,
// 上传的 APK 文件大小
contentLength: contentLength,
// 大陆地区可以忽略,海外地区必填
chineseMainlandFlag: 0,
},
}
)
第三部,开始上传 APK 文件
我们要求是只需要上传到平台就行,所以不用搞分片上传。这一步就是坑的开始,如果你按照文档来,百分百会给你报一串错误 XML,如下:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Error>
<Code>SignatureDoesNotMatch</Code>
<Message>The request signature we calculated does not match the signature you provided. Check your
key and signing method.</Message>
<RequestId>0000019AED024D5C94169C0FA01D180A</RequestId>
<HostId>Z9v+cC1sRnaWw6x0vi8pxxYA0YVnKxbYHUPAFpnxkX8sLV44u5b02Z+ailn2wCnR</HostId>
<AWSAccessKeyId>NT1DSSQ7R3FSAEMCBELN</AWSAccessKeyId>
<SignatureProvided>fbd22dc7ed5bf90a420fcd6bd78b3fae93e68f2668758c53cbb13c7555913422</SignatureProvided>
<StringToSign>AWS4-HMAC-SHA256
20251205T053539Z
20251205/ap-southeast-3/s3/aws4_request
0b4bc19ab88df2d905cfbba6f3f24fd722da7cb7434fc9d2288776eaed7cd15f</StringToSign>
<StringToSignBytes>41 57 53 34 2d 48 4d 41 43 2d 53 48 41 32 35 36 0a 32 30 32 35 31 32 30 35 54
30 35 33 35 33 39 5a 0a 32 30 32 35 31 32 30 35 2f 61 70 2d 73 6f 75 74 68 65 61 73 74 2d 33 2f
73 33 2f 61 77 73 34 5f 72 65 71 75 65 73 74 0a 30 62 34 62 63 31 39 61 62 38 38 64 66 32 64 39
30 35 63 66 62 62 61 36 66 33 66 32 34 66 64 37 32 32 64 61 37 63 62 37 34 33 34 66 63 39 64 32
32 38 38 37 37 36 65 61 65 64 37 63 64 31 35 66</StringToSignBytes>
<CanonicalRequest>PUT
/SG/2025120505/1764912939540-2257a16f-c80f-4fe3-bdde-f3150531d3c4.apk
content-type:application/octet-stream
host:nsp-appgallery-agcfs-dra.obs.ap-southeast-3.myhuaweicloud.com
x-amz-content-sha256:UNSIGNED-PAYLOAD
x-amz-date:20251205T053539Z
ontent-length;content-type;host;x-amz-content-sha256;x-amz-date
UNSIGNED-PAYLOAD
</CanonicalRequest>
<StringToSignBytes>50 55 54 0a 2f 53 47 2f 32 30 32 35 31 32 30 35 30 35 2f 31 37 36 34 39 31 32
39 33 39 35 34 30 2d 32 32 35 37 61 31 36 66 2d 63 38 30 66 2d 34 66 65 33 2d 62 64 64 65 2d 66
33 31 35 30 35 33 31 64 33 63 34 2e 61 70 6b 0a 0a 63 6f 6e 74 65 6e 74 2d 74 79 70 65 3a 61 70
70 6c 69 63 61 74 69 6f 6e 2f 6f 63 74 65 74 2d 73 74 72 65 61 6d 0a 68 6f 73 74 3a 6e 73 70 2d
61 70 70 67 61 6c 6c 65 72 79 2d 61 67 63 66 73 2d 64 72 61 2e 6f 62 73 2e 61 70 2d 73 6f 75 74
68 65 61 73 74 2d 33 2e 6d 79 68 75 61 77 65 69 63 6c 6f 75 64 2e 63 6f 6d 0a 78 2d 61 6d 7a 2d
63 6f 6e 74 65 6e 74 2d 73 68 61 32 35 36 3a 55 4e 53 49 47 4e 45 44 2d 50 41 59 4c 4f 41 44 0a
78 2d 61 6d 7a 2d 64 61 74 65 3a 32 30 32 35 31 32 30 35 54 30 35 33 35 33 39 5a 0a 0a 63 6f 6e
74 65 6e 74 2d 6c 65 6e 67 74 68 3b 63 6f 6e 74 65 6e 74 2d 74 79 70 65 3b 68 6f 73 74 3b 78 2d
61 6d 7a 2d 63 6f 6e 74 65 6e 74 2d 73 68 61 32 35 36 3b 78 2d 61 6d 7a 2d 64 61 74 65 0a 55 4e
53 49 47 4e 45 44 2d 50 41 59 4c 4f 41 44</StringToSignBytes>
</Error>
其实问题的症结就在于,文档没告诉你,你需要把Content-Length放入头部,他的头部签名字段x-amz-content-sha256中需要Content-Length来进行签名。
// 第二步拿到的上传结果
const result = uploadApplyResult;
// 文件流
const fileStream = fs.createReadStream(apkFilePath);
// 现在不会报错了...
const response = await axios.put(result.urlInfo.url, fileStream, {
headers: {
...result.urlInfo.headers,
'Content-Length': contentLength
}
});
第四步,更新应用文件信息
// 第二步拿到的结果
const objectId = uploadApplyResult.urlInfo.objectId;
const fileName = 'my.apk'
const fileInfoResult = await httpClient.put(
`https://connect-api.cloud.huawei.com/api/publish/v2/app-file-info?appId=${appId}`,
{
// apk: 5, aab: 6
'fileType': 5,
'files': [{
'fileName': fileName,
'fileDestUrl': result.urlInfo.objectId
}]
},
);
最后
我看到截止目前 2025-12-25 为止,AppGallery 已经从 v2 升级到了 v3。所以,搞 API 没啥前途。你看到上面所谓的 SignatureDoesNotMatch 后,能第一时间想到看是不是少了或者多了参数,这种解决问题的直觉才是我认为最重要的。
ps: 英文版文档还没改,用的还是 v2 的 API。