@@ -430,6 +430,12 @@ class FormHttpErrorsWidget extends StatelessWidget {
430430 Widget build (BuildContext context) {
431431 final theme = Theme .of (context);
432432
433+ // A JSON endpoint answering with HTML often means a proxy or bot wall such
434+ // as Anubis intercepted the request. Such pages render near-blank through
435+ // flutter_html since their visible content is JS-driven. In these cases we
436+ // try pull out the <title>, which usually names the culprit
437+ final htmlTitle = exception.type == ErrorType .html ? htmlErrorTitle (exception.htmlError) : null ;
438+
433439 return Container (
434440 constraints: const BoxConstraints (maxHeight: 250 ),
435441 decoration: BoxDecoration (
@@ -441,9 +447,18 @@ class FormHttpErrorsWidget extends StatelessWidget {
441447 child: Column (
442448 children: [
443449 Icon (Icons .error_outline, color: theme.colorScheme.error),
444- if (exception.type == ErrorType .html)
445- ServerHtmlError (data: exception.htmlError)
446- else
450+ if (exception.type == ErrorType .html) ...[
451+ if (htmlTitle != null )
452+ Text (
453+ htmlTitle,
454+ textAlign: TextAlign .center,
455+ style: TextStyle (
456+ fontWeight: FontWeight .bold,
457+ color: theme.colorScheme.error,
458+ ),
459+ ),
460+ ServerHtmlError (data: exception.htmlError),
461+ ] else
447462 ...formatApiErrors (
448463 extractErrors (exception.errors),
449464 color: theme.colorScheme.error,
@@ -454,3 +469,25 @@ class FormHttpErrorsWidget extends StatelessWidget {
454469 );
455470 }
456471}
472+
473+ /// Extracts the `<title>` of an HTML error body (e.g. a proxy or bot-wall page
474+ /// such as Cloudflare or Anubis), with the few HTML entities that show up in
475+ /// such titles unescaped. Returns null when there is no usable title.
476+ String ? htmlErrorTitle (String html) {
477+ final match = RegExp (
478+ r'<title[^>]*>(.*?)</title>' ,
479+ caseSensitive: false ,
480+ dotAll: true ,
481+ ).firstMatch (html);
482+ final raw = match? .group (1 )? .trim ();
483+ if (raw == null || raw.isEmpty) {
484+ return null ;
485+ }
486+ return raw
487+ .replaceAll (''' , "'" )
488+ .replaceAll (''' , "'" )
489+ .replaceAll ('"' , '"' )
490+ .replaceAll ('<' , '<' )
491+ .replaceAll ('>' , '>' )
492+ .replaceAll ('&' , '&' );
493+ }
0 commit comments