使用httpx调用APNS推送更新Wallet Pass时遇RemoteProtocolError求助
解决APNS推送Wallet Pass时的httpx.RemoteProtocolError问题
我正在开发一个Web服务,通过APNS推送通知更新Wallet Pass。因为httpx支持HTTP/2,所以用它写了测试代码,但运行时抛出httpx.RemoteProtocolError错误,附上完整报错栈,需要解决这个问题以成功向APNS发送请求。
测试代码
import httpx import ssl import asyncio async def send_push(): context = ssl.create_default_context() context.load_verify_locations(cafile = "/Users/valley/Desktop/External_Finder/Learning_Centers_Development/Arcade-Pass/certs/passcertificate.pem") payload = { "aps" : "" } async with httpx.AsyncClient(http2 = True, cert = "/Users/valley/Desktop/External_Finder/Learning_Centers_Development/Arcade-Pass/certs/ArcadePassCertKey.pem") as client: r = await client.post("https://api.sandbox.push.apple.com:2197/3/device/4c526af3e9cd29cc0c6f8954de5f68fd1d00348696fe4a984581e35f19fe1ddf", data = payload) print(r.http_version) print(r.text) asyncio.run(send_push())
报错信息
Traceback (most recent call last): File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/httpx/_transports/default.py", line 60, in map_httpcore_exceptions yield File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/httpx/_transports/default.py", line 353, in handle_async_request resp = await self._pool.handle_async_request(req) File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/httpcore/_async/connection_pool.py", line 253, in handle_async_request raise exc File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/httpcore/_async/connection_pool.py", line 237, in handle_async_request response = await connection.handle_async_request(request) File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/httpcore/_async/connection.py", line 90, in handle_async_request return await self._connection.handle_async_request(request) File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/httpcore/_async/http2.py", line 146, in handle_async_request raise exc File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/httpcore/_async/http2.py", line 114, in handle_async_request status, headers = await self._receive_response( File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/httpcore/_async/http2.py", line 231, in _receive_response event = await self._receive_stream_event(request, stream_id) File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/httpcore/_async/http2.py", line 262, in _receive_stream_event await self._receive_events(request, stream_id) File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/httpcore/_async/http2.py", line 291, in _receive_events raise RemoteProtocolError(event) httpcore.RemoteProtocolError: <ConnectionTerminated error_code:ErrorCodes.NO_ERROR, last_stream_id:0, additional_data:7b22726561736f6e223a22426164436572746966> The above exception was the direct cause of the following exception: Traceback (most recent call last): File "test_push_notifications.py", line 24, in <module> asyncio.run(send_push()) File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/asyncio/runners.py", line 43, in run return loop.run_until_complete(main) File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/asyncio/base_events.py", line 608, in run_until_complete return future.result() File "test_push_notifications.py", line 19, in send_push r = await client.post("https://api.sandbox.push.apple.com:2197/3/device/4c526af3e9cd29cc0c6f8954de5f68fd1d00348696fe4a984581e35f19fe1ddf", data = payload) File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/httpx/_client.py", line 1848, in post return await self.request( File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/httpx/_client.py", line 1533, in request return await self.send(request, auth=auth, follow_redirects=follow_redirects) File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/httpx/_client.py", line 1620, in send response = await self._send_handling_auth( File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/httpx/_client.py", line 1648, in _send_handling_auth response = await self._send_handling_redirects( File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/httpx/_client.py", line 1685, in _send_handling_redirects response = await self._send_single_request(request) File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/httpx/_client.py", line 1722, in _send_single_request response = await transport.handle_async_request(request) File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/httpx/_transports/default.py", line 353, in handle_async_request resp = await self._pool.handle_async_request(req) File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/contextlib.py", line 131, in __exit__ self.gen.throw(type, value, traceback) File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/httpx/_transports/default.py", line 77, in map_httpcore_exceptions raise mapped_exc(message) from exc httpx.RemoteProtocolError: <ConnectionTerminated error_code:ErrorCodes.NO_ERROR, last_stream_id:0, additional_data:7b22726561736f6e223a22426164436572746966>
问题分析与修复方案
1. 证书配置错误
APNS要求客户端提供证书+私钥的组合用于身份验证:
- 若
ArcadePassCertKey.pem仅为私钥文件,需将证书文件(passcertificate.pem)和私钥文件合并为一个PEM文件,或通过httpx的cert参数传入元组形式的证书路径和私钥路径。 - 自定义SSL上下文加载验证证书是多余的,httpx默认会使用系统根证书完成APNS服务器的身份验证,可删除这部分代码。
2. Payload格式与请求参数错误
- APNS要求请求体为合法JSON,当前
"aps": ""不符合规范,Wallet Pass的静默推送Payload应使用{"aps": {}}。 - 发送请求时需用
json=payload而非data=payload,data参数会将字典转为表单格式,而APNS仅接受JSON格式的请求体。
3. 缺少必要的HTTP请求头
HTTP/2协议下的APNS请求必须指定两个关键Header:
apns-push-type: 对于Wallet Pass推送,值为passbook。apns-topic: 对应你的Wallet Pass在苹果开发者后台配置的Bundle ID,必须准确匹配。
修复后的代码
import httpx import asyncio async def send_push(): # 证书配置:传入证书与私钥的路径元组,若证书有密码则添加第三个元素(密码字符串) cert = ( "/Users/valley/Desktop/External_Finder/Learning_Centers_Development/Arcade-Pass/certs/passcertificate.pem", "/Users/valley/Desktop/External_Finder/Learning_Centers_Development/Arcade-Pass/certs/ArcadePassCertKey.pem" ) # 符合APNS规范的Wallet Pass推送Payload payload = { "aps": {} } # 必要的请求头配置 headers = { "apns-push-type": "passbook", "apns-topic": "com.yourcompany.yourapp.pass" # 替换为实际的Pass Bundle ID } async with httpx.AsyncClient(http2=True, cert=cert) as client: r = await client.post( "https://api.sandbox.push.apple.com:2197/3/device/4c526af3e9cd29cc0c6f8954de5f68fd1d00348696fe4a984581e35f19fe1ddf", json=payload, headers=headers ) print(r.http_version) print(r.status_code) print(r.text) asyncio.run(send_push())
额外注意事项
- 确保设备Token为沙箱环境的有效Token,且设备已安装对应的Wallet Pass。
- 若证书设置了密码,需在
cert元组中添加密码参数,例如cert=(cert_path, key_path, "your-cert-password")。 - 检查苹果开发者后台的推送权限是否已正确配置,确保证书与Bundle ID关联。
内容的提问来源于stack exchange,提问作者Yash Kothari
相关产品推荐
相关产品推荐

