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))