fix(blender): send a real User-Agent so Cloudflare stops blocking the add-on
Deploy / deploy (push) Successful in 5m59s
Deploy / deploy (push) Successful in 5m59s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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/<version>`). 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/*`.
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user