Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1afe452
Structured errors draft - failing tests
AndrewAsseily Oct 6, 2025
c46cf3e
Merge branch 'v2' into nyandrew/structured-error
AndrewAsseily Oct 22, 2025
6842e4e
Revert "Structured errors draft - failing tests"
AndrewAsseily Oct 24, 2025
3fe5630
Merge branch 'v2' into nyandrew/structured-error
AndrewAsseily Nov 11, 2025
bcc33fc
Structured Errors implementation Draft 1
AndrewAsseily Nov 14, 2025
829968b
Refactor structured error
AndrewAsseily Nov 17, 2025
181b94a
Merge branch 'v2' into nyandrew/structured-error
AndrewAsseily Nov 17, 2025
6958d85
Remove cli_hide_error_details
AndrewAsseily Nov 17, 2025
9eef83f
Merge branch 'v2' into nyandrew/structured-error
AndrewAsseily Nov 21, 2025
329d167
Implement --output off format to suppress stdout
AndrewAsseily Nov 25, 2025
5700443
Merge branch 'v2' into nyandrew/structured-error
AndrewAsseily Nov 25, 2025
073c28d
Add missing off status to output choices
AndrewAsseily Nov 26, 2025
60b04ee
Merge branch 'v2' into nyandrew/structured-error
AndrewAsseily Dec 1, 2025
b82ef33
Catch and format ClientError exceptions raised during pagination
AndrewAsseily Dec 1, 2025
4b34b62
Merge branch 'v2' into nyandrew/structured-error
AndrewAsseily Dec 1, 2025
7f511a2
Update test fixtures and documentation for 'off' output format option
AndrewAsseily Dec 2, 2025
ae6987d
Remove TODO for filtering; use --output off for sensitive data instead
AndrewAsseily Dec 4, 2025
9db425e
Add changelog entry for structured error output
AndrewAsseily Dec 8, 2025
bc25515
Moves structured error handling into ClientErrorHandler to write erro…
AndrewAsseily Dec 14, 2025
ce86991
Remove cli_error_format from global options docs (config-only variable)
AndrewAsseily Dec 14, 2025
29ee23c
Add structured error output with configurable formats (json, yaml, en…
AndrewAsseily Dec 17, 2025
e003f09
Update changelog entry to reflect new error format settings
AndrewAsseily Dec 17, 2025
4f87cfa
Update validation text and format relevant files
AndrewAsseily Dec 17, 2025
64a79eb
Refactor error handling: simplify logic, add constants
AndrewAsseily Dec 17, 2025
a7548f6
Update documentation to include --cli-error-format global option
AndrewAsseily Dec 17, 2025
3c14ea4
Fix legacy format test to verify modeled fields are not displayed
AndrewAsseily Dec 17, 2025
408c0c6
Fix error handler double construction and improve error format messaging
AndrewAsseily Dec 17, 2025
8c77925
Extract service-specific error fields from top-level response
AndrewAsseily Dec 17, 2025
8a4c74b
Remove text format from complex value suggestions due to poor array h…
AndrewAsseily Dec 18, 2025
aa19e40
Simplified the format_error method to always use consistent formatting
AndrewAsseily Dec 18, 2025
b4db2ae
Add the new error format to the general options table in config-vars
AndrewAsseily Dec 18, 2025
4232a97
Restore error handler fallback
AndrewAsseily Dec 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/next-release/enhancement-Output-51826.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "enhancement",
"category": "Output",
"description": "Add support for ``--output off`` to suppress all stdout output while preserving stderr for errors and warnings."
}
5 changes: 5 additions & 0 deletions .changes/next-release/feature-Output-59989.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "feature",
"category": "Output",
"description": "Add structured error output with configurable formats. AWS service errors now display additional fields in the configured format (legacy, json, yaml, text, table, or enhanced). Configure via ``--cli-error-format``, ``cli_error_format`` config variable, or ``AWS_CLI_ERROR_FORMAT`` environment variable. The new enhanced format is the default. Set ``cli_error_format=legacy`` to preserve the original error format."
}
32 changes: 27 additions & 5 deletions awscli/clidriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
ScopedConfigProvider,
)
from botocore.context import start_as_current_context
from botocore.exceptions import ClientError
from botocore.history import get_global_history_recorder

from awscli import __version__
Expand Down Expand Up @@ -119,7 +120,7 @@ def create_clidriver(args=None):
session.full_config.get('plugins', {}),
event_hooks=session.get_component('event_emitter'),
)
error_handlers_chain = construct_cli_error_handlers_chain()
error_handlers_chain = construct_cli_error_handlers_chain(session)
driver = CLIDriver(
session=session, error_handler=error_handlers_chain, debug=debug
)
Expand Down Expand Up @@ -246,7 +247,9 @@ def __init__(self, session=None, error_handler=None, debug=False):
self.session = session
self._error_handler = error_handler
if self._error_handler is None:
self._error_handler = construct_cli_error_handlers_chain()
self._error_handler = construct_cli_error_handlers_chain(
self.session
)
if debug:
self._set_logging(debug)
self._update_config_chain()
Expand Down Expand Up @@ -275,6 +278,9 @@ def _update_config_chain(self):
config_store.set_config_provider(
'cli_help_output', self._construct_cli_help_output_chain()
)
config_store.set_config_provider(
'cli_error_format', self._construct_cli_error_format_chain()
)

def _construct_cli_region_chain(self):
providers = [
Expand Down Expand Up @@ -368,6 +374,20 @@ def _construct_cli_auto_prompt_chain(self):
]
return ChainProvider(providers=providers)

def _construct_cli_error_format_chain(self):
providers = [
EnvironmentProvider(
name='AWS_CLI_ERROR_FORMAT',
env=os.environ,
),
ScopedConfigProvider(
config_var_name='cli_error_format',
session=self.session,
),
ConstantProvider(value='enhanced'),
]
return ChainProvider(providers=providers)

@property
def subcommand_table(self):
return self._get_command_table()
Expand Down Expand Up @@ -516,12 +536,16 @@ def main(self, args=None):
command_table = self._get_command_table()
parser = self.create_parser(command_table)
self._add_aliases(command_table, parser)

try:
# Because _handle_top_level_args emits events, it's possible
# that exceptions can be raised, which should have the same
# general exception handling logic as calling into the
# command table. This is why it's in the try/except clause.
parsed_args, remaining = parser.parse_known_args(args)
self._error_handler = construct_cli_error_handlers_chain(
self.session, parsed_args
)
self._handle_top_level_args(parsed_args)
validate_preferred_output_encoding()
self._emit_session_event(parsed_args)
Expand Down Expand Up @@ -1028,9 +1052,7 @@ def _make_client_call(
paginator = client.get_paginator(py_operation_name)
response = paginator.paginate(**parameters)
else:
response = getattr(client, xform_name(operation_name))(
**parameters
)
response = getattr(client, py_operation_name)(**parameters)
return response

def _display_response(self, command_name, response, parsed_globals):
Expand Down
14 changes: 13 additions & 1 deletion awscli/data/cli.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"text",
"table",
"yaml",
"yaml-stream"
"yaml-stream",
"off"
],
"help": "<p>The formatting style for command output.</p>"
},
Expand Down Expand Up @@ -85,6 +86,17 @@
"no-cli-auto-prompt": {
"action": "store_true",
"help": "<p>Disable automatically prompt for CLI input parameters.</p>"
},
"cli-error-format": {
"choices": [
"legacy",
"json",
"yaml",
"text",
"table",
"enhanced"
],
"help": "<p>The formatting style for error output. By default, errors are displayed in enhanced format.</p>"
}
}
}
164 changes: 162 additions & 2 deletions awscli/errorhandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
import argparse
import logging
import signal

Expand All @@ -36,10 +37,64 @@
ConfigurationError,
ParamValidationError,
)
from awscli.formatter import get_formatter
from awscli.utils import PagerInitializationException

LOG = logging.getLogger(__name__)

VALID_ERROR_FORMATS = ['legacy', 'json', 'yaml', 'text', 'table', 'enhanced']
# Maximum number of items to display inline for collections
MAX_INLINE_ITEMS = 5


class EnhancedErrorFormatter:
def format_error(self, error_info, formatted_message, stream):
stream.write(f'{formatted_message}\n')

additional_fields = self._get_additional_fields(error_info)

if not additional_fields:
return

stream.write('\nAdditional error details:\n')
for key, value in additional_fields.items():
if self._is_simple_value(value):
stream.write(f'{key}: {value}\n')
elif self._is_small_collection(value):
stream.write(f'{key}: {self._format_inline(value)}\n')
else:
stream.write(
f'{key}: <complex value>\n'
f'(Use --cli-error-format with json or yaml '
f'to see full details)\n'
)

def _is_simple_value(self, value):
return isinstance(value, (str, int, float, bool, type(None)))

def _is_small_collection(self, value):
if isinstance(value, list):
return len(value) < MAX_INLINE_ITEMS and all(
self._is_simple_value(item) for item in value
)
elif isinstance(value, dict):
return len(value) < MAX_INLINE_ITEMS and all(
self._is_simple_value(v) for v in value.values()
)
return False

def _format_inline(self, value):
if isinstance(value, list):
return f"[{', '.join(str(item) for item in value)}]"
elif isinstance(value, dict):
items = ', '.join(f'{k}: {v}' for k, v in value.items())
return f'{{{items}}}'
return str(value)

def _get_additional_fields(self, error_info):
standard_keys = {'Code', 'Message'}
return {k: v for k, v in error_info.items() if k not in standard_keys}


def construct_entry_point_handlers_chain():
handlers = [
Expand All @@ -51,7 +106,7 @@ def construct_entry_point_handlers_chain():
return ChainedExceptionHandler(exception_handlers=handlers)


def construct_cli_error_handlers_chain():
def construct_cli_error_handlers_chain(session=None, parsed_globals=None):
handlers = [
ParamValidationErrorsHandler(),
UnknownArgumentErrorHandler(),
Expand All @@ -60,7 +115,7 @@ def construct_cli_error_handlers_chain():
NoCredentialsErrorHandler(),
PagerErrorHandler(),
InterruptExceptionHandler(),
ClientErrorHandler(),
ClientErrorHandler(session, parsed_globals),
GeneralExceptionHandler(),
]
return ChainedExceptionHandler(exception_handlers=handlers)
Expand Down Expand Up @@ -108,6 +163,111 @@ class ClientErrorHandler(FilteredExceptionHandler):
EXCEPTIONS_TO_HANDLE = ClientError
RC = CLIENT_ERROR_RC

def __init__(self, session=None, parsed_globals=None):
self._session = session
self._parsed_globals = parsed_globals
self._enhanced_formatter = None

def _do_handle_exception(self, exception, stdout, stderr):
displayed_structured = False
if self._session:
displayed_structured = self._try_display_structured_error(
exception, stderr
)

if not displayed_structured:
return super()._do_handle_exception(exception, stdout, stderr)

return self.RC

def _resolve_error_format(self, parsed_globals):
if parsed_globals:
error_format = getattr(parsed_globals, 'cli_error_format', None)
if error_format:
return error_format.lower()
try:
error_format = self._session.get_config_variable(
'cli_error_format'
)
if error_format:
return error_format.lower()
except (KeyError, AttributeError) as e:
LOG.debug(
'Failed to get cli_error_format from config: %s', e
)

return 'enhanced'

def _try_display_structured_error(self, exception, stderr):
try:
error_response = self._extract_error_response(exception)
if not error_response or 'Error' not in error_response:
return False

error_info = error_response['Error']
error_format = self._resolve_error_format(self._parsed_globals)

if error_format not in VALID_ERROR_FORMATS:
LOG.warning(
f"Invalid cli_error_format: '{error_format}'. "
f"Using 'enhanced' format."
)
error_format = 'enhanced'

if error_format == 'legacy':
return False

formatted_message = str(exception)

if error_format == 'enhanced':
if self._enhanced_formatter is None:
self._enhanced_formatter = EnhancedErrorFormatter()
self._enhanced_formatter.format_error(
error_info, formatted_message, stderr
)
return True

temp_parsed_globals = argparse.Namespace()
temp_parsed_globals.output = error_format
temp_parsed_globals.query = None
temp_parsed_globals.color = (
getattr(self._parsed_globals, 'color', 'auto')
if self._parsed_globals
else 'auto'
)

formatter = get_formatter(error_format, temp_parsed_globals)
formatter('error', error_info, stderr)
return True

except Exception as e:
LOG.debug(
'Failed to display structured error: %s', e, exc_info=True
)
return False

@staticmethod
def _extract_error_response(exception):
if not isinstance(exception, ClientError):
return None

if hasattr(exception, 'response') and 'Error' in exception.response:
error_dict = dict(exception.response['Error'])

# AWS services return modeled error fields
# at the top level of the error response,
# not nested under an Error key. Botocore preserves this structure.
# Include these fields to provide complete error information.
# Exclude response metadata and avoid duplicates.
excluded_keys = {'Error', 'ResponseMetadata', 'Code', 'Message'}
for key, value in exception.response.items():
if key not in excluded_keys and key not in error_dict:
error_dict[key] = value

return {'Error': error_dict}

return None


class ConfigurationErrorHandler(FilteredExceptionHandler):
EXCEPTIONS_TO_HANDLE = ConfigurationError
Expand Down
20 changes: 20 additions & 0 deletions awscli/examples/global_options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@

* yaml-stream

* off


``--query`` (string)

Expand Down Expand Up @@ -96,3 +98,21 @@

Disable automatically prompt for CLI input parameters.

``--cli-error-format`` (string)

The formatting style for error output. By default, errors are displayed in enhanced format.


* legacy

* json

* yaml

* text

* table

* enhanced


1 change: 1 addition & 0 deletions awscli/examples/global_synopsis.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@
[--no-cli-pager]
[--cli-auto-prompt]
[--no-cli-auto-prompt]
[--cli-error-format <value>]
Loading
Loading