From 3733c0c4fa7a92e1104ea9c3330be935f3cfd6ce Mon Sep 17 00:00:00 2001 From: twotalesanimation <80506065+twotalesanimation@users.noreply.github.com> Date: Sat, 15 Aug 2026 19:11:52 +0200 Subject: [PATCH] fix(blender): send a real User-Agent so Cloudflare stops blocking the add-on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Submitting failed with a 403 before the request reached the server: Cloudflare error 1010, a browser-signature ban. Python's urllib identifies itself as "Python-urllib/3.x", which is exactly the scripted signature that rule targets. The AE panel was unaffected because curl sends curl/8.x, and the worker because .NET sends its own. Verified against the live zone, same request and token, only the agent changed: Python-urllib/3.11 gets 403 while VFXReviewBlender/1.0.1 gets 200 — as do curl and a browser agent. Naming the tool is also the honest thing: the studio's logs should say what is calling them, and the version travels with it. A Cloudflare block is also no longer reported to the artist as a wall of JSON about browser signatures — it says the firewall stopped the request before it reached the server, and to ask the pipeline TD. Add-on 1.0.1. Co-Authored-By: Claude Opus 5 --- BlenderPanel/README.md | 8 ++++++++ BlenderPanel/vfxreview_submit.py | 27 ++++++++++++++++++++++++--- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/BlenderPanel/README.md b/BlenderPanel/README.md index 50b1137..1782473 100644 --- a/BlenderPanel/README.md +++ b/BlenderPanel/README.md @@ -86,3 +86,11 @@ nothing in the UI sets them today — one place for render settings is the point **Exit code 0 with no frames is treated as a failure.** Blender exits cleanly when a frame range is empty or an output path is unwritable, so the worker checks that frames were actually saved before reporting success. + +**The add-on sends its own User-Agent** (`VFXReviewBlender/`). Python's +`urllib` otherwise identifies itself as `Python-urllib/3.x`, which Cloudflare +blocks outright as a scripted signature — a 403 with error 1010 before the +request ever reaches the server. Verified against the live zone: `Python-urllib` +gets 403, the add-on's own agent gets 200. If a future Cloudflare rule blocks it +anyway, the add-on says so in plain language rather than showing the raw JSON, +and the fix is a WAF allow rule on the agent or on `/api/ext/*`. diff --git a/BlenderPanel/vfxreview_submit.py b/BlenderPanel/vfxreview_submit.py index d54a39e..e6ca717 100644 --- a/BlenderPanel/vfxreview_submit.py +++ b/BlenderPanel/vfxreview_submit.py @@ -1,7 +1,7 @@ bl_info = { "name": "VFXReview — Submit Render", "author": "Two Tales", - "version": (1, 0, 0), + "version": (1, 0, 1), "blender": (3, 6, 0), "location": "Properties > Output > VFXReview", "description": "Queue this .blend on the studio render farm instead of rendering locally", @@ -48,6 +48,10 @@ def prefs(): return bpy.context.preferences.addons[__name__].preferences +ADDON_VERSION = ".".join(str(n) for n in bl_info["version"]) +USER_AGENT = "VFXReviewBlender/%s (Blender %s)" % (ADDON_VERSION, bpy.app.version_string) + + # ── API ─────────────────────────────────────────────────────────────────────── def api_request(path, payload=None, method="GET"): @@ -61,6 +65,11 @@ def api_request(path, payload=None, method="GET"): req = urllib.request.Request(url, data=data, method=method) req.add_header("Authorization", "Bearer " + p.api_key) req.add_header("Accept", "application/json") + # urllib identifies itself as "Python-urllib/3.x" by default, which + # Cloudflare blocks outright as a scripted signature (error 1010). Sending a + # real name for this tool is also the honest thing: the studio's logs should + # say what is calling them. + req.add_header("User-Agent", USER_AGENT) if data: req.add_header("Content-Type", "application/json") @@ -70,9 +79,21 @@ def api_request(path, payload=None, method="GET"): except urllib.error.HTTPError as e: body = e.read().decode("utf-8", "replace") try: - message = json.loads(body).get("error", body) + parsed = json.loads(body) except Exception: - message = body + parsed = {} + + # A Cloudflare block is not a pipeline error and reads nothing like one, + # so say what it actually is rather than showing the artist a wall of + # JSON about browser signatures. + if parsed.get("cloudflare_error") or parsed.get("error_code") == 1010: + raise RuntimeError( + "Blocked by Cloudflare before reaching the server (error %s). " + "The studio's firewall needs to allow this add-on — ask the pipeline TD." + % parsed.get("error_code", e.code) + ) + + message = parsed.get("error", body) if parsed else body raise RuntimeError("Server said %s: %s" % (e.code, message)) except urllib.error.URLError as e: raise RuntimeError("Could not reach %s (%s)" % (url, e.reason))