This is an automated email from the ASF dual-hosted git repository.

beto pushed a commit to branch cloudflare-error-page
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 7edac00cb6794a335f8c5eac32354bdc62fa09a2
Author: Beto Dealmeida <[email protected]>
AuthorDate: Tue Dec 16 15:33:14 2025 -0500

    fun: CloudFlare error page
---
 superset/templates/cloudflare_error.html | 184 +++++++++++++++++++++++++++++++
 superset/views/error_handling.py         | 169 ++++++++++++++++++++++++----
 2 files changed, 329 insertions(+), 24 deletions(-)

diff --git a/superset/templates/cloudflare_error.html 
b/superset/templates/cloudflare_error.html
new file mode 100644
index 0000000000..c7b307e0ba
--- /dev/null
+++ b/superset/templates/cloudflare_error.html
@@ -0,0 +1,184 @@
+{# Custom Cloudflare error page template with inlined CSS #}
+<!DOCTYPE html>
+<!--[if lt IE 7]> <html class="no-js ie6 oldie" lang="en-US"> <![endif]-->
+<!--[if IE 7]>    <html class="no-js ie7 oldie" lang="en-US"> <![endif]-->
+<!--[if IE 8]>    <html class="no-js ie8 oldie" lang="en-US"> <![endif]-->
+<!--[if gt IE 8]><!--> <html class="no-js" lang="en-US"> <!--<![endif]-->
+<head>
+{% set error_code = params.error_code or 500 %}
+{% set title = params.title or 'Internal server error' %}
+{% set html_title = params.html_title or ((error_code | string) + ': ' + 
title) %}
+<title>{{ html_title }}</title>
+<meta charset="UTF-8" />
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
+<meta name="robots" content="noindex, nofollow" />
+<meta name="viewport" content="width=device-width,initial-scale=1" />
+<style>
+*{box-sizing:border-box}
+body{margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,Segoe 
UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica 
Neue,sans-serif;font-size:15px;line-height:1.6;color:#404040;background:#fff}
+a{color:#2f7bbf;text-decoration:none}
+a:hover{color:#f48120;text-decoration:underline}
+h1,h2,h3,h4,h5{margin:0;font-weight:400}
+p{margin:0 0 1em}
+.cf-wrapper{max-width:960px;margin:0 auto}
+.cf-header{padding:40px 15px 25px}
+.cf-header h1{font-size:56px;font-weight:200;line-height:1.2;color:#222}
+.cf-header h1 span{display:inline-block}
+.cf-code-label{font-size:14px;font-weight:600;background:#f0f0f0;color:#666;padding:3px
 10px;border-radius:3px;margin-left:15px;vertical-align:middle}
+.cf-header-meta{margin-top:12px;font-size:14px;color:#666}
+.cf-status-section{background:linear-gradient(90deg,#f7f7f7 0%,#fff 
50%,#f7f7f7 100%);padding:30px 0}
+.cf-status-row{display:flex;align-items:center;justify-content:center;max-width:720px;margin:0
 auto;padding:0 15px}
+.cf-status-item{flex:1;text-align:center;padding:20px 10px}
+.cf-status-icon{position:relative;width:90px;height:90px;margin:0 auto 12px}
+.cf-status-badge{position:absolute;bottom:0;left:50%;transform:translateX(-50%);width:28px;height:28px;border-radius:50%;border:3px
 solid #fff;box-shadow:0 1px 3px rgba(0,0,0,.15)}
+.cf-status-badge.ok{background:#9bca3e}
+.cf-status-badge.error{background:#cf3a32}
+.cf-status-badge 
svg{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:14px;height:14px}
+.cf-status-label{font-size:13px;color:#888;margin-bottom:2px}
+.cf-status-name{font-size:22px;color:#666;font-weight:300}
+.cf-status-text{font-size:18px;font-weight:500;margin-top:2px}
+.cf-status-text.ok{color:#9bca3e}
+.cf-status-text.error{color:#cf3a32}
+.cf-arrow{flex:0 0 60px;height:2px;background:#ccc;position:relative;margin:0 
-5px;margin-top:-30px}
+.cf-arrow:after{content:'';position:absolute;right:-1px;top:-5px;width:0;height:0;border:6px
 solid transparent;border-left-color:#ccc}
+.cf-arrow.error{background:#cf3a32}
+.cf-arrow.error:after{border-left-color:#cf3a32}
+.cf-content{padding:35px 15px;max-width:960px;margin:0 auto}
+.cf-content-row{display:flex;flex-wrap:wrap;gap:40px}
+.cf-content-col{flex:1;min-width:280px}
+.cf-content-col h2{font-size:26px;margin-bottom:12px;color:#333}
+.cf-content-col h5{font-size:15px;font-weight:600;margin:16px 0 6px;color:#333}
+.cf-content-col p{color:#555;line-height:1.7}
+.cf-footer{padding:18px 15px;border-top:1px solid 
#eee;font-size:13px;color:#777;text-align:center}
+.cf-footer-item{margin:0 8px}
+.cf-footer 
button{background:none;border:none;color:#2f7bbf;cursor:pointer;font-size:13px;padding:0}
+.cf-footer button:hover{text-decoration:underline}
+.cf-icon-browser{background-image:url("data:image/svg+xml,%3Csvg 
xmlns='http://www.w3.org/2000/svg' viewBox='0 0 90 90'%3E%3Crect x='8' y='15' 
width='74' height='55' rx='4' fill='%23f1f1f1' stroke='%23ddd' 
stroke-width='2'/%3E%3Crect x='8' y='15' width='74' height='14' rx='4' 
fill='%23ddd'/%3E%3Ccircle cx='20' cy='22' r='4' fill='%23ff6058'/%3E%3Ccircle 
cx='32' cy='22' r='4' fill='%23ffc130'/%3E%3Ccircle cx='44' cy='22' r='4' 
fill='%2327ca40'/%3E%3Crect x='18' y='38' width='54' height='6 [...]
+.cf-icon-cloud{background-image:url("data:image/svg+xml,%3Csvg 
xmlns='http://www.w3.org/2000/svg' viewBox='0 0 90 70'%3E%3Cpath d='M72.5 
55H20c-8.3 0-15-6.7-15-15 0-7.4 5.4-13.6 12.4-14.8C18.8 15.2 27.6 8 38 8c9.5 0 
17.6 5.9 20.9 14.2 1.4-.3 2.9-.5 4.4-.5 9.7 0 17.6 7.6 18.2 17.1C87.2 40.5 92 
46.5 92 53.5c0 8.6-6.9 15.5-15.5 15.5h-4z' fill='%23f6821f'/%3E%3Cpath d='M60.8 
55H25.5C18.6 55 13 49.4 13 42.5c0-6.2 4.5-11.3 10.4-12.3C24.5 22.4 31.4 17 39.5 
17c7.4 0 13.7 4.6 16.3 11.1 1.1-.2 2.2 [...]
+.cf-icon-server{background-image:url("data:image/svg+xml,%3Csvg 
xmlns='http://www.w3.org/2000/svg' viewBox='0 0 70 90'%3E%3Crect x='5' y='8' 
width='60' height='22' rx='3' fill='%23f1f1f1' stroke='%23ddd' 
stroke-width='2'/%3E%3Crect x='5' y='34' width='60' height='22' rx='3' 
fill='%23f1f1f1' stroke='%23ddd' stroke-width='2'/%3E%3Crect x='5' y='60' 
width='60' height='22' rx='3' fill='%23f1f1f1' stroke='%23ddd' 
stroke-width='2'/%3E%3Ccircle cx='18' cy='19' r='5' 
fill='%2327ca40'/%3E%3Ccircl [...]
+@media (max-width:720px){
+.cf-header h1{font-size:36px}
+.cf-code-label{display:block;margin:10px 0 0;width:fit-content}
+.cf-status-row{flex-direction:column}
+.cf-status-item{padding:15px 10px}
+.cf-arrow{display:none}
+.cf-content-row{flex-direction:column}
+}
+</style>
+</head>
+<body>
+<div class="cf-wrapper">
+  <div class="cf-header">
+    <h1>
+      <span>{{ title }}</span>
+      <span class="cf-code-label">Error code {{ error_code }}</span>
+    </h1>
+    <div class="cf-header-meta">
+      Visit <a href="https://www.cloudflare.com/"; target="_blank" 
rel="noopener noreferrer">cloudflare.com</a> for more information.
+    </div>
+    <div class="cf-header-meta">{{ params.time }}</div>
+  </div>
+</div>
+
+{% set browser_status = params.browser_status.status or 'ok' %}
+{% set cloudflare_status = params.cloudflare_status.status or 'ok' %}
+{% set host_status = params.host_status.status or 'ok' %}
+
+<div class="cf-status-section">
+  <div class="cf-status-row">
+    <div class="cf-status-item">
+      <div class="cf-status-icon">
+        <div class="cf-icon-browser"></div>
+        <div class="cf-status-badge {{ browser_status }}">
+          {% if browser_status == 'ok' %}
+          <svg viewBox="0 0 14 14" fill="none"><path d="M3 7l3 3 5-6" 
stroke="#fff" stroke-width="2" stroke-linecap="round" 
stroke-linejoin="round"/></svg>
+          {% else %}
+          <svg viewBox="0 0 14 14" fill="none"><path d="M3 3l8 8M11 3l-8 8" 
stroke="#fff" stroke-width="2" stroke-linecap="round"/></svg>
+          {% endif %}
+        </div>
+      </div>
+      <div class="cf-status-label">You</div>
+      <div class="cf-status-name">Browser</div>
+      <div class="cf-status-text {{ browser_status }}">{{ 
params.browser_status.status_text or ('Working' if browser_status == 'ok' else 
'Error') }}</div>
+    </div>
+
+    <div class="cf-arrow {{ 'error' if browser_status != 'ok' else '' 
}}"></div>
+
+    <div class="cf-status-item">
+      <div class="cf-status-icon">
+        <div class="cf-icon-cloud"></div>
+        <div class="cf-status-badge {{ cloudflare_status }}">
+          {% if cloudflare_status == 'ok' %}
+          <svg viewBox="0 0 14 14" fill="none"><path d="M3 7l3 3 5-6" 
stroke="#fff" stroke-width="2" stroke-linecap="round" 
stroke-linejoin="round"/></svg>
+          {% else %}
+          <svg viewBox="0 0 14 14" fill="none"><path d="M3 3l8 8M11 3l-8 8" 
stroke="#fff" stroke-width="2" stroke-linecap="round"/></svg>
+          {% endif %}
+        </div>
+      </div>
+      <div class="cf-status-label">{{ params.cloudflare_status.location or 
'San Francisco' }}</div>
+      <div class="cf-status-name">Cloudflare</div>
+      <div class="cf-status-text {{ cloudflare_status }}">{{ 
params.cloudflare_status.status_text or ('Working' if cloudflare_status == 'ok' 
else 'Error') }}</div>
+    </div>
+
+    <div class="cf-arrow {{ 'error' if host_status != 'ok' else '' }}"></div>
+
+    <div class="cf-status-item">
+      <div class="cf-status-icon">
+        <div class="cf-icon-server"></div>
+        <div class="cf-status-badge {{ host_status }}">
+          {% if host_status == 'ok' %}
+          <svg viewBox="0 0 14 14" fill="none"><path d="M3 7l3 3 5-6" 
stroke="#fff" stroke-width="2" stroke-linecap="round" 
stroke-linejoin="round"/></svg>
+          {% else %}
+          <svg viewBox="0 0 14 14" fill="none"><path d="M3 3l8 8M11 3l-8 8" 
stroke="#fff" stroke-width="2" stroke-linecap="round"/></svg>
+          {% endif %}
+        </div>
+      </div>
+      <div class="cf-status-label">{{ params.host_status.location or 
'superset.io' }}</div>
+      <div class="cf-status-name">Host</div>
+      <div class="cf-status-text {{ host_status }}">{{ 
params.host_status.status_text or ('Working' if host_status == 'ok' else 
'Error') }}</div>
+    </div>
+  </div>
+</div>
+
+<div class="cf-content">
+  <div class="cf-content-row">
+    <div class="cf-content-col">
+      <h2>What happened?</h2>
+      {{ params.what_happened | safe }}
+    </div>
+    <div class="cf-content-col">
+      <h2>What can I do?</h2>
+      {{ params.what_can_i_do | safe }}
+    </div>
+  </div>
+</div>
+
+<div class="cf-wrapper">
+  <div class="cf-footer">
+    <span class="cf-footer-item">Cloudflare Ray ID: <strong>{{ params.ray_id 
}}</strong></span>
+    &bull;
+    <span class="cf-footer-item" id="cf-footer-item-ip">Your IP: <button 
type="button" id="cf-footer-ip-reveal">Click to reveal</button><span 
id="cf-footer-ip" class="hidden">{{ params.client_ip }}</span></span>
+    &bull;
+    {% if params.perf_sec_by %}
+    <span class="cf-footer-item">{{ params.perf_sec_by.text or 'Performance & 
security by' }} <a href="{{ params.perf_sec_by.link_url or 
'https://www.cloudflare.com/' }}" target="_blank" rel="noopener noreferrer">{{ 
params.perf_sec_by.link_text or 'Cloudflare' }}</a></span>
+    {% else %}
+    <span class="cf-footer-item">Performance &amp; security by <a 
href="https://www.cloudflare.com/5xx-error-landing"; target="_blank" 
rel="noopener noreferrer">Cloudflare</a></span>
+    {% endif %}
+  </div>
+</div>
+<script>
+(function(){
+  var btn = document.getElementById('cf-footer-ip-reveal');
+  var ip = document.getElementById('cf-footer-ip');
+  if (btn && ip) {
+    btn.addEventListener('click', function() {
+      btn.style.display = 'none';
+      ip.style.display = 'inline';
+    });
+  }
+})();
+</script>
+<style>.hidden{display:none}</style>
+</body>
+</html>
diff --git a/superset/views/error_handling.py b/superset/views/error_handling.py
index 236291d96e..84a7dc0a04 100644
--- a/superset/views/error_handling.py
+++ b/superset/views/error_handling.py
@@ -19,17 +19,17 @@ from __future__ import annotations
 import dataclasses
 import functools
 import logging
+import secrets
 import typing
-from importlib.resources import files
 from typing import Any, Callable, cast
 
 from flask import (
     Flask,
     request,
     Response,
-    send_file,
 )
 from flask_wtf.csrf import CSRFError
+from jinja2 import Environment, PackageLoader
 from sqlalchemy import exc
 from werkzeug.exceptions import HTTPException
 
@@ -54,6 +54,127 @@ logger = logging.getLogger(__name__)
 
 JSON_MIMETYPE = "application/json; charset=utf-8"
 
+# Set up Jinja2 environment for Cloudflare error template
+_cf_env = Environment(
+    loader=PackageLoader("superset", "templates"),
+    autoescape=True,
+)
+_cf_template = _cf_env.get_template("cloudflare_error.html")
+
+# Error code to title mapping for Cloudflare-style error pages
+CLOUDFLARE_ERROR_TITLES: dict[int, str] = {
+    400: "Bad Request",
+    401: "Access Denied",
+    403: "Access Denied",
+    404: "Web page is not found",
+    405: "Method Not Allowed",
+    408: "Request Timeout",
+    429: "Too Many Requests",
+    500: "Internal Server Error",
+    502: "Bad Gateway",
+    503: "Service Temporarily Unavailable",
+    504: "Gateway Timeout",
+}
+
+# What happened descriptions for each error code
+CLOUDFLARE_WHAT_HAPPENED: dict[int, str] = {
+    400: """The server could not understand your request due to invalid syntax.
+            Please check your request and try again.""",
+    401: """This page requires authentication. You need to log in to access
+            this resource.""",
+    403: """You don't have permission to access this resource. The owner of
+            this website has banned your access based on your credentials.""",
+    404: """The page you requested could not be found. It may have been moved,
+            deleted, or never existed in the first place.""",
+    405: """The request method is not supported for the requested resource.""",
+    408: """The server timed out waiting for your request. Please try 
again.""",
+    429: """You've made too many requests in a short period of time.
+            Please slow down and try again later.""",
+    500: """There is an unknown connection issue between Superset and the
+            origin web server. As a result, the web page can not be 
displayed.""",
+    502: """Superset was unable to get a valid response from the upstream 
server.""",
+    503: """The server is temporarily unable to handle your request due to
+            maintenance or capacity problems. Please try again later.""",
+    504: """Superset was unable to get a response from the upstream server
+            in time.""",
+}
+
+
+def render_cloudflare_error_page(
+    error_code: int,
+    error_message: str | None = None,
+) -> str:
+    """
+    Render a Cloudflare-style error page for the given error code.
+
+    Args:
+        error_code: HTTP status code
+        error_message: Optional custom error message to display
+
+    Returns:
+        Rendered HTML string for the error page
+    """
+    from datetime import datetime, timezone
+
+    # Generate ray ID and get client IP
+    ray_id = request.headers.get("Cf-Ray", secrets.token_hex(8))[:16]
+    client_ip = request.headers.get("X-Forwarded-For", request.remote_addr or 
"Unknown")
+    utc_now = datetime.now(timezone.utc)
+    time_str = utc_now.strftime("%Y-%m-%d %H:%M:%S UTC")
+
+    title = CLOUDFLARE_ERROR_TITLES.get(error_code, "Error")
+    what_happened = error_message or CLOUDFLARE_WHAT_HAPPENED.get(
+        error_code,
+        "An unexpected error occurred while processing your request.",
+    )
+
+    # Determine status indicators based on error type
+    host = request.host or "superset.io"
+    if error_code >= 500:
+        # Server errors: browser and Cloudflare OK, host has issue
+        browser_status = {"status": "ok"}
+        cloudflare_status = {"status": "ok", "location": "San Francisco"}
+        host_status = {"status": "error", "location": host}
+    elif error_code in (401, 403):
+        # Auth errors: browser has issue (needs auth)
+        browser_status = {"status": "error"}
+        cloudflare_status = {"status": "ok", "location": "San Francisco"}
+        host_status = {"status": "ok", "location": host}
+    else:
+        # Client errors (404, etc): browser has issue
+        browser_status = {"status": "error"}
+        cloudflare_status = {"status": "ok", "location": "San Francisco"}
+        host_status = {"status": "ok", "location": host}
+
+    params = {
+        "html_title": f"superset.io | {error_code}: {title}",
+        "title": title,
+        "error_code": error_code,
+        "time": time_str,
+        "ray_id": ray_id,
+        "client_ip": client_ip,
+        "browser_status": browser_status,
+        "cloudflare_status": cloudflare_status,
+        "host_status": host_status,
+        "what_happened": f"<p>{what_happened}</p>",
+        "what_can_i_do": """
+            <h5>If you are a visitor of this website:</h5>
+            <p>Please try again in a few minutes. If you continue to see this
+            error, you can contact the site administrator.</p>
+            <h5>If you are the owner of this website:</h5>
+            <p>Check your Superset logs for more information about this error.
+            You may need to restart the service or check your database
+            connections.</p>
+        """,
+        "perf_sec_by": {
+            "text": "Performance & security by",
+            "link_text": "Apache Superset",
+            "link_url": "https://superset.apache.org";,
+        },
+    }
+
+    return _cf_template.render(params=params)
+
 
 def get_error_level_from_status(
     status_code: int,
@@ -157,18 +278,16 @@ def set_app_error_handlers(app: Flask) -> None:  # noqa: 
C901
     @app.errorhandler(HTTPException)
     def show_http_exception(ex: HTTPException) -> FlaskResponse:
         logger.warning("HTTPException", exc_info=True)
+        error_code = ex.code or 500
 
-        if (
-            "text/html" in request.accept_mimetypes
-            and not app.config["DEBUG"]
-            and ex.code in {404, 500}
-        ):
-            path = files("superset") / f"static/assets/{ex.code}.html"
-            # Try to serve HTML file; fall back to JSON if not built
-            try:
-                return send_file(path, max_age=0), ex.code
-            except FileNotFoundError:
-                pass
+        if "text/html" in request.accept_mimetypes:
+            return Response(
+                render_cloudflare_error_page(
+                    error_code, utils.error_msg_from_exception(ex)
+                ),
+                status=error_code,
+                mimetype="text/html",
+            )
 
         return json_error_response(
             [
@@ -178,7 +297,7 @@ def set_app_error_handlers(app: Flask) -> None:  # noqa: 
C901
                     level=ErrorLevel.ERROR,
                 ),
             ],
-            status=ex.code or 500,
+            status=error_code,
         )
 
     @app.errorhandler(CommandException)
@@ -190,13 +309,12 @@ def set_app_error_handlers(app: Flask) -> None:  # noqa: 
C901
         """
         logger.warning("CommandException", exc_info=True)
 
-        if "text/html" in request.accept_mimetypes and not app.config["DEBUG"]:
-            path = files("superset") / "static/assets/500.html"
-            # Try to serve HTML file; fall back to JSON if not built
-            try:
-                return send_file(path, max_age=0), 500
-            except FileNotFoundError:
-                pass
+        if "text/html" in request.accept_mimetypes:
+            return Response(
+                render_cloudflare_error_page(ex.status, ex.message),
+                status=ex.status,
+                mimetype="text/html",
+            )
 
         extra = ex.normalized_messages() if isinstance(ex, 
CommandInvalidError) else {}
         return json_error_response(
@@ -218,9 +336,12 @@ def set_app_error_handlers(app: Flask) -> None:  # noqa: 
C901
         logger.warning("Exception", exc_info=True)
         logger.exception(ex)
 
-        if "text/html" in request.accept_mimetypes and not app.config["DEBUG"]:
-            path = files("superset") / "static/assets/500.html"
-            return send_file(path, max_age=0), 500
+        if "text/html" in request.accept_mimetypes:
+            return Response(
+                render_cloudflare_error_page(500, 
utils.error_msg_from_exception(ex)),
+                status=500,
+                mimetype="text/html",
+            )
 
         return json_error_response(
             [

Reply via email to