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

树莓派Pico W调用Azure ML REST端点推理无响应问题排查

树莓派Pico W调用Azure ML REST端点无响应问题排查与解决

问题背景

在Python 3.10环境下可正常调用Azure ML模型的REST端点完成推理,但在Raspberry Pi Pico W的MicroPython环境中,使用urllib.urequests.urlopen()返回空结果,也无法像标准Python那样结合Request对象使用。试过urequests、urllib.urequests、uurequests三个库均无效,调用response.content时返回空值或超时,其内部的raw.read()执行失败。

可正常运行的Python代码

import urllib.request
import json
import os
import ssl

def allowSelfSignedHttps(allowed):
    # 绕过客户端的服务器证书验证
    if allowed and not os.environ.get('PYTHONHTTPSVERIFY', '') and getattr(ssl, '_create_unverified_context', None):
        ssl._create_default_https_context = ssl._create_unverified_context

allowSelfSignedHttps(True) # 如果评分服务使用自签名证书,需要此行代码

# 请求数据
data =  {
  "input_data": {
    "columns": [
      "PLAT_PH",
      "PLAT_ORP",
      "PSU1",
      "PSU2",
      "PSU3"
    ],
    "index": [0],
    "data": [[3.0, 600, 4.0, 5.0, 0]]
  }
}

body = str.encode(json.dumps(data))

url = 'my-azure-endpoint-url'
# 替换为端点的主/次密钥或AMLToken
api_key = 'my-api-key'
if not api_key:
    raise Exception("调用端点需要提供密钥")

# azureml-model-deployment头部会强制请求指向特定部署
# 删除此头部将遵循端点流量规则
headers = {'Content-Type':'application/json', 'Authorization':('Bearer '+ api_key), 'azureml-model-deployment': 'platypus-ph-orp-ps-yn-02-1' }

req = urllib.request.Request(url, body, headers)

try:
    response = urllib.request.urlopen(req)
    result = response.read()
    print(result)
except urllib.error.HTTPError as error:
    print("请求失败,状态码: " + str(error.code))
    # 打印头部(包含请求ID和时间戳,有助于调试失败原因)
    print(error.info())
    print(error.read().decode("utf8", 'ignore'))

尝试的MicroPython代码

body = str.encode(json.dumps(data))
headers = {'Content-Type':'application/json', 'Authorization':('Bearer '+ api_key), 'azureml-model-deployment': 'my-azure-deployment' }

response = urequests.request('POST', url, data=body, headers=headers)
print("response: ", response)
result = response.content
print("result: ", result)

使用的urequests库源码

import usocket

class Response:

    def __init__(self, f):
        self.raw = f
        self.encoding = "utf-8"
        self._cached = None

    def close(self):
        if self.raw:
            self.raw.close()
            self.raw = None
        self._cached = None

    @property
    def content(self):
        if self._cached is None:
            try:
                self._cached = self.raw.read()
            finally:
                self.raw.close()
                self.raw = None
        return self._cached

    @property
    def text(self):
        return str(self.content, self.encoding)

    def json(self):
        import ujson
        return ujson.loads(self.content)


def request(method, url, data=None, json=None, headers={}, stream=None):
    try:
        proto, dummy, host, path = url.split("/", 3)
    except ValueError:
        proto, dummy, host = url.split("/", 2)
        path = ""
    if proto == "http:":
        port = 80
    elif proto == "https:":
        import ussl
        port = 443
    else:
        raise ValueError("Unsupported protocol: " + proto)

    if ":" in host:
        host, port = host.split(":", 1)
        port = int(port)

    ai = usocket.getaddrinfo(host, port, 0, usocket.SOCK_STREAM)
    ai = ai[0]

    s = usocket.socket(ai[0], ai[1], ai[2])
    try:
        s.connect(ai[-1])
        if proto == "https:":
            s = ussl.wrap_socket(s, server_hostname=host)
        s.write(b"%s /%s HTTP/1.0\r\n" % (method, path))
        if not "Host" in headers:
            s.write(b"Host: %s\r\n" % host)
        # Iterate over keys to avoid tuple alloc
        for k in headers:
            s.write(k)
            s.write(b": ")
            s.write(headers[k])
            s.write(b"\r\n")
        if json is not None:
            assert data is None
            import ujson
            data = ujson.dumps(json)
            s.write(b"Content-Type: application/json\r\n")
        if data:
            s.write(b"Content-Length: %d\r\n" % len(data))
        s.write(b"\r\n")
        if data:
            s.write(data)

        l = s.readline()
        #print(l)
        l = l.split(None, 2)
        status = int(l[1])
        reason = ""
        if len(l) > 2:
            reason = l[2].rstrip()
        while True:
            l = s.readline()
            if not l or l == b"\r\n":
                break
            #print(l)
            if l.startswith(b"Transfer-Encoding:"):
                if b"chunked" in l:
                    raise ValueError("Unsupported " + l)
            elif l.startswith(b"Location:") and not 200 <= status <= 299:
                raise NotImplementedError("Redirects not yet supported")
    except OSError:
        s.close()
        raise

    resp = Response(s)
    resp.status_code = status
    resp.reason = reason
    return resp

def head(url, **kw):
    return request("HEAD", url, **kw)

def get(url, **kw):
    return request("GET", url, **kw)

def post(url, **kw):
    return request("POST", url, **kw)

def put(url, **kw):
    return request("PUT", url, **kw)

def patch(url, **kw):
    return request("PATCH", url, **kw)

def delete(url, **kw):
    return request("DELETE", url, **kw)

问题分析与修复方案

核心问题点

  1. HTTPS证书验证缺失:原Python代码禁用了自签名证书验证,但MicroPython默认会验证证书,导致连接失败
  2. HTTP协议版本兼容性:当前urequests使用HTTP/1.0,Azure端点对HTTP/1.1支持更完善
  3. 字符串与字节类型不兼容:发送请求时未将字符串类型的头部、方法转换为字节,导致MicroPython执行异常
  4. 响应读取可靠性差:一次性读取整个响应容易超时或失败
  5. 无超时设置与异常捕获:无法定位连接超时、证书错误等具体问题

修复后的urequests库代码

import usocket

class Response:

    def __init__(self, f):
        self.raw = f
        self.encoding = "utf-8"
        self._cached = None

    def close(self):
        if self.raw:
            self.raw.close()
            self.raw = None
        self._cached = None

    @property
    def content(self):
        if self._cached is None:
            try:
                # 分块读取响应,提升可靠性
                self._cached = b""
                while True:
                    chunk = self.raw.read(1024)
                    if not chunk:
                        break
                    self._cached += chunk
            finally:
                self.raw.close()
                self.raw = None
        return self._cached

    @property
    def text(self):
        return str(self.content, self.encoding)

    def json(self):
        import ujson
        return ujson.loads(self.content)


def request(method, url, data=None, json=None, headers={}, stream=None, timeout=10):
    try:
        proto, dummy, host, path = url.split("/", 3)
    except ValueError:
        proto, dummy, host = url.split("/", 2)
        path = ""
    if proto == "http:":
        port = 80
        use_ssl = False
    elif proto == "https:":
        import ussl
        port = 443
        use_ssl = True
    else:
        raise ValueError("不支持的协议: " + proto)

    if ":" in host:
        host, port = host.split(":", 1)
        port = int(port)

    ai = usocket.getaddrinfo(host, port, 0, usocket.SOCK_STREAM)
    ai = ai[0]

    s = usocket.socket(ai[0], ai[1], ai[2])
    s.settimeout(timeout)
    try:
        s.connect(ai[-1])
        if use_ssl:
            # 禁用证书验证,匹配原Python代码逻辑
            s = ussl.wrap_socket(s, server_hostname=host, cert_reqs=ussl.CERT_NONE)
        # 使用HTTP/1.1协议,提升兼容性
        s.write(b"%s /%s HTTP/1.1\r\n" % (method.encode(), path.encode()))
        if not "Host" in headers:
            s.write(b"Host: %s\r\n" % host.encode())
        # 确保所有头部都是字节类型
        for k, v in headers.items():
            s.write(k.encode() if isinstance(k, str) else k)
            s.write(b": ")
            s.write(v.encode() if isinstance(v, str) else v)
            s.write(b"\r\n")
        if json is not None:
            assert data is None
            import ujson
            data = ujson.dumps(json)
            if "Content-Type" not in headers:
                s.write(b"Content-Type: application/json\r\n")
        if data:
            if isinstance(data, str):
                data = data.encode()
            s.write(b"Content-Length: %d\r\n" % len(data))
        # 添加HTTP/1.1必需的连接关闭头部
        s.write(b"Connection: close\r\n")
        s.write(b"\r\n")
        if data:
            s.write(data)

        l = s.readline()
        if not l:
            raise OSError("读取响应头失败")
        l = l.split(None, 2)
        status = int(l[1])
        reason = ""
        if len(l) > 2:
            reason = l[2].rstrip().decode()
        while True:
            l = s.readline()
            if not l or l == b"\r\n":
                break
            if l.startswith(b"Transfer-Encoding:"):
                if b"chunked" in l:
                    raise ValueError("不支持分块编码: " + l.decode())
            elif l.startswith(b"Location:") and not 200 <= status <= 299:
                raise NotImplementedError("暂不支持重定向")
    except OSError as e:
        s.close()
        raise e

    resp = Response(s)
    resp.status_code = status
    resp.reason = reason
    return resp

def head(url, **kw):
    return request("HEAD", url, **kw)

def get(url, **kw):
    return request("GET", url, **kw)

def post(url, **kw):
    return request("POST", url, **kw)

def put(url, **kw):
    return request("PUT", url, **kw)

def patch(url, **kw):
    return request("PATCH", url, **kw)

def delete(url, **kw):
    return request("DELETE", url, **kw)

修复后的调用代码

import ujson
import urequests

# 定义请求数据
data =  {
  "input_data": {
    "columns": [
      "PLAT_PH",
      "PLAT_ORP",
      "PSU1",
      "PSU2",
      "PSU3"
    ],
    "index": [0],
    "data": [[3.0, 600, 4.0, 5.0, 0]]
  }
}

url = 'your-azure-endpoint-url'
api_key = 'your-api-key'
headers = {
    'Content-Type':'application/json', 
    'Authorization':'Bearer ' + api_key, 
    'azureml-model-deployment': 'your-deployment-name'
}

try:
    # 使用json参数自动处理序列化,避免手动编码
    response = urequests.post(url, json=data, headers=headers, timeout=15)
    print(f"响应状态码: {response.status_code}")
    print(f"响应内容: {response.text}")
    # 解析JSON结果
    result = response.json()
    print(f"解析后的结果: {result}")
except OSError as e:
    print(f"请求出错: {e}")
except Exception as e:
    print(f"其他错误: {e}")

关键修复说明

  • 禁用证书验证:在ussl.wrap_socket中添加cert_reqs=ussl.CERT_NONE,匹配原Python代码的证书绕过逻辑
  • 升级HTTP协议:改用HTTP/1.1并添加Connection: close头部,适配Azure端点要求
  • 类型兼容处理:自动将字符串类型的请求内容转换为字节,避免MicroPython类型错误
  • 分块读取响应:分块读取响应内容,解决一次性读取超时或失败问题
  • 添加超时与异常捕获:设置socket超时,添加完整异常捕获,方便定位问题

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 01:04:55