fix: show friendly error message when GitHub is down (#3655)

This commit is contained in:
Eric Seidel
2026-03-20 23:11:34 -07:00
committed by GitHub
parent a48df6e6de
commit 989bca0a1c
2 changed files with 126 additions and 2 deletions
@@ -1,4 +1,4 @@
// cspell:words unmatch
// cspell:words unmatch githubstatus
import 'dart:io';
import 'package:scoped_deps/scoped_deps.dart';
@@ -10,6 +10,44 @@ final gitRef = create(Git.new);
/// The [Git] instance available in the current zone.
Git get git => read(gitRef);
/// {@template git_server_unreachable_exception}
/// Exception thrown when a git command fails due to a server error.
/// {@endtemplate}
class GitServerUnreachableException extends ProcessException {
/// {@macro git_server_unreachable_exception}
GitServerUnreachableException(
super.executable,
super.arguments, [
super.message = '',
super.errorCode = 0,
]);
}
/// Pattern that matches HTTP server errors (500, 502, 503, 504) or "Internal
/// Server Error" in git output.
final _serverErrorPattern = RegExp(
'The requested URL returned error: (500|502|503|504)|Internal Server Error',
);
/// Pattern that extracts the hostname from a git remote URL in stderr.
/// Matches URLs like `https://github.com/org/repo.git/`.
final _remoteHostPattern = RegExp(r"https?://([^/']+)");
String _buildServerErrorMessage(String stderr) {
final hostMatch = _remoteHostPattern.firstMatch(stderr);
final host = hostMatch?.group(1);
final serverName = host ?? 'the remote git server';
final buffer = StringBuffer('Unable to reach $serverName.');
if (host == 'github.com') {
buffer.write(
'\nIf your network connection is working, check '
'https://www.githubstatus.com for service status.',
);
}
return buffer.toString();
}
/// A wrapper around all git related functionality.
class Git {
/// Name of the git executable.
@@ -26,10 +64,19 @@ class Git {
workingDirectory: workingDirectory,
);
if (result.exitCode != 0) {
final stderr = '${result.stderr}';
if (_serverErrorPattern.hasMatch(stderr)) {
throw GitServerUnreachableException(
executable,
arguments,
_buildServerErrorMessage(stderr),
result.exitCode,
);
}
throw ProcessException(
executable,
arguments,
'${result.stderr}',
stderr,
result.exitCode,
);
}
@@ -1,3 +1,4 @@
// cspell:words githubstatus
import 'dart:io';
import 'package:mason_logger/mason_logger.dart';
@@ -37,6 +38,82 @@ void main() {
when(() => processResult.exitCode).thenReturn(ExitCode.success.code);
});
group('server error detection', () {
test('includes github.com and status link for GitHub URLs', () async {
when(() => processResult.exitCode).thenReturn(128);
when(() => processResult.stderr).thenReturn(
"fatal: unable to access 'https://github.com/"
"shorebirdtech/flutter.git/': "
'The requested URL returned error: 500',
);
expect(
() => runWithOverrides(
() => git.clone(
url: 'https://github.com/shorebirdtech/flutter.git',
outputDirectory: './output',
),
),
throwsA(
isA<GitServerUnreachableException>().having(
(e) => e.message,
'message',
allOf(contains('github.com'), contains('githubstatus.com')),
),
),
);
});
test('includes host name for non-GitHub URLs', () async {
when(() => processResult.exitCode).thenReturn(128);
when(() => processResult.stderr).thenReturn(
"fatal: unable to access 'https://gitlab.com/"
"org/repo.git/': "
'The requested URL returned error: 502',
);
expect(
() => runWithOverrides(() => git.fetch(directory: 'repo')),
throwsA(
isA<GitServerUnreachableException>().having(
(e) => e.message,
'message',
allOf(
contains('gitlab.com'),
isNot(contains('githubstatus.com')),
),
),
),
);
});
test('falls back to generic message when no URL in stderr', () async {
when(() => processResult.exitCode).thenReturn(128);
when(
() => processResult.stderr,
).thenReturn('remote: Internal Server Error');
expect(
() => runWithOverrides(() => git.fetch(directory: 'repo')),
throwsA(
isA<GitServerUnreachableException>().having(
(e) => e.message,
'message',
contains('the remote git server'),
),
),
);
});
test('throws ProcessException for non-server errors', () async {
when(() => processResult.exitCode).thenReturn(128);
when(
() => processResult.stderr,
).thenReturn('fatal: repository not found');
expect(
() => runWithOverrides(() => git.fetch(directory: 'repo')),
throwsA(isA<ProcessException>()),
);
});
});
group('clone', () {
const url = 'https://github.com/shorebirdtech/shorebird';
const outputDirectory = './output';