You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by be...@apache.org on 2025/12/16 20:33:25 UTC

(superset) branch cloudflare-error-page created (now 7edac00cb6)

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

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


      at 7edac00cb6 fun: CloudFlare error page

This branch includes the following new commits:

     new 7edac00cb6 fun: CloudFlare error page

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(superset) 01/01: fun: CloudFlare error page

Posted by be...@apache.org.
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 <ro...@dealmeida.net>
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(
             [