Python 3.14.8 urllib credential fix: a stdlib Sume job poll script

Python 3.14.8 fixed urllib HTTPPasswordMgr handing credentials across schemes. Upgrade, and call Sume with an x-api-key header from a plain stdlib script.

5 min readSume
All posts

Python 3.14.8 is an expedited security release published on September 30, and its notes list a fix for urllib HTTPPasswordMgr handling of credentials across schemes, alongside fixes for ssl, tarfile, zipfile and an Expat update to 2.8.5 (Python 3.14.8 release page, read 2026-10-04). If you run Python scripts that call the Sume API with only the standard library, upgrade to 3.14.8 on the 3.14 line. Then check that no password manager sits in the request path at all.

A Sume call should not involve one. The API takes the key in a header, either Authorization: Bearer ... or x-api-key: sume_live_..., and the API overview says to send one of them. Sending both is rejected with a 401. Because the key is a header and the base URL is always https://api.sume.com, there is no Basic auth realm for a password manager to match against.

What to check in an existing script

  • Search for HTTPPasswordMgr, HTTPBasicAuthHandler and build_opener in code that talks to Sume. None should be needed.
  • Make sure the key never appears in the URL. Keep it in the header and read it from the environment.
  • Pin the runtime in your container image or CI matrix so the patched release is the one that runs.
  • Re-run your poll loop once after the upgrade, since the TLS and HTTP-adjacent fixes in a security release touch code your script uses.

A stdlib submit-and-poll script

The script below submits an image job with an Idempotency-Key, polls GET /v1/jobs/{id}/status until terminal is true, and prints the result when result_ready is true. It honours next_poll_after_seconds and never sleeps less than two seconds. It exits at once if the key is not set. The endpoints and the three-step flow come from the jobs and results guide.

import json, os, sys, time, urllib.request, uuid

KEY = os.environ.get("SUME_API_KEY", "")
if not KEY:
    sys.exit("set SUME_API_KEY")

def call(method, path, body=None, extra=None):
    headers = {"x-api-key": KEY, "content-type": "application/json", **(extra or {})}
    data = json.dumps(body).encode() if body is not None else None
    req = urllib.request.Request("https://api.sume.com" + path, data=data,
                                 method=method, headers=headers)
    with urllib.request.urlopen(req, timeout=60) as res:
        payload = json.load(res)
    return payload.get("data", payload)

prompt = "Product hero shot of a matte black bottle on marble"
sub = call("POST", "/v1/image-1.0/generate", {"prompt": prompt, "mode": "async"},
           {"Idempotency-Key": str(uuid.uuid4())})
job_id = sub["request_id"]
print("job", job_id)
while True:
    st = call("GET", f"/v1/jobs/{job_id}/status")
    if st.get("terminal"):
        break
    time.sleep(max(2, st.get("next_poll_after_seconds") or 2))
if st.get("result_ready"):
    print(json.dumps(call("GET", f"/v1/jobs/{job_id}/result"), indent=2))
else:
    print("ended without a result:", st.get("sume_status"))

Handling errors without a library

urlopen raises HTTPError for any 4xx or 5xx, and the body of that error is still the JSON envelope with code, request_id and retryable. Read it with json.load(error) before you decide what to do. A 402 insufficient_credits should stop the script, and a 429 should wait for the server's retry_after_seconds before the same call goes out again with the same Idempotency-Key. A new key on a retry would create a second job and a second charge.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume