diff --git a/CHANGELOG.md b/CHANGELOG.md index b5778b4a..c02c9c07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). - Added support for basic JSON types on `--form`/`--multipart` when using JSON only operators (`:=`/`:=@`). ([#1212](https://github.com/httpie/httpie/issues/1212)) - Added support for automatically enabling `--stream` when `Content-Type` is `text/event-stream`. ([#376](https://github.com/httpie/httpie/issues/376)) - Added new `pie-dark`/`pie-light` (and `pie`) styles that match with [HTTPie for Web and Desktop](https://httpie.io/product). ([#1237](https://github.com/httpie/httpie/issues/1237)) +- Added support for better error handling on DNS failures. ([#1248](https://github.com/httpie/httpie/issues/1248)) - Broken plugins will no longer crash the whole application. ([#1204](https://github.com/httpie/httpie/issues/1204)) - Fixed auto addition of XML declaration to every formatted XML response. ([#1156](https://github.com/httpie/httpie/issues/1156)) - Fixed highlighting when `Content-Type` specifies `charset`. ([#1242](https://github.com/httpie/httpie/issues/1242)) diff --git a/httpie/core.py b/httpie/core.py index 48d21bc4..bc03686b 100644 --- a/httpie/core.py +++ b/httpie/core.py @@ -2,6 +2,7 @@ import argparse import os import platform import sys +import socket from typing import List, Optional, Tuple, Union, Callable import requests @@ -21,6 +22,7 @@ from .models import ( from .output.writer import write_message, write_stream, MESSAGE_SEPARATOR_BYTES from .plugins.registry import plugin_manager from .status import ExitStatus, http_status_to_exit_status +from .utils import unwrap_context # noinspection PyDefaultArgument @@ -41,6 +43,21 @@ def raw_main( include_debug_info = '--debug' in args include_traceback = include_debug_info or '--traceback' in args + def handle_generic_error(e, annotation=None): + msg = str(e) + if hasattr(e, 'request'): + request = e.request + if hasattr(request, 'url'): + msg = ( + f'{msg} while doing a {request.method}' + f' request to URL: {request.url}' + ) + if annotation: + msg += annotation + env.log_error(f'{type(e).__name__}: {msg}') + if include_traceback: + raise + if include_debug_info: print_debug_info(env) if args == ['--debug']: @@ -90,19 +107,23 @@ def raw_main( f'Too many redirects' f' (--max-redirects={parsed_args.max_redirects}).' ) + except requests.exceptions.ConnectionError as exc: + annotation = None + original_exc = unwrap_context(exc) + if isinstance(original_exc, socket.gaierror): + if original_exc.errno == socket.EAI_AGAIN: + annotation = '\nCouldn\'t connect to a DNS server. Perhaps check your connection and try again.' + elif original_exc.errno == socket.EAI_NONAME: + annotation = '\nCouldn\'t resolve the given hostname. Perhaps check it and try again.' + propagated_exc = original_exc + else: + propagated_exc = exc + + handle_generic_error(propagated_exc, annotation=annotation) + exit_status = ExitStatus.ERROR except Exception as e: # TODO: Further distinction between expected and unexpected errors. - msg = str(e) - if hasattr(e, 'request'): - request = e.request - if hasattr(request, 'url'): - msg = ( - f'{msg} while doing a {request.method}' - f' request to URL: {request.url}' - ) - env.log_error(f'{type(e).__name__}: {msg}') - if include_traceback: - raise + handle_generic_error(e) exit_status = ExitStatus.ERROR return exit_status diff --git a/httpie/utils.py b/httpie/utils.py index 8669de8c..fa19fa7c 100644 --- a/httpie/utils.py +++ b/httpie/utils.py @@ -229,3 +229,11 @@ def split(iterable: Iterable[T], key: Callable[[T], bool]) -> Tuple[List[T], Lis else: right.append(item) return left, right + + +def unwrap_context(exc: Exception) -> Optional[Exception]: + context = exc.__context__ + if isinstance(context, Exception): + return unwrap_context(context) + else: + return exc diff --git a/tests/test_errors.py b/tests/test_errors.py index 5a1a0f24..fca48fff 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -1,3 +1,5 @@ +import pytest +import socket from unittest import mock from pytest import raises from requests import Request @@ -31,6 +33,21 @@ def test_error_traceback(program): http('--traceback', 'www.google.com') +@mock.patch('httpie.core.program') +@pytest.mark.parametrize("error_code, expected_message", [ + (socket.EAI_AGAIN, "check your connection"), + (socket.EAI_NONAME, "check the URL"), +]) +def test_error_custom_dns(program, error_code, expected_message): + exc = ConnectionError('Connection aborted') + exc.__context__ = socket.gaierror(error_code, "") + program.side_effect = exc + + r = http('www.google.com', tolerate_error_exit_status=True) + assert r.exit_status == ExitStatus.ERROR + assert expected_message in r.stderr + + def test_max_headers_limit(httpbin_both): with raises(ConnectionError) as e: http('--max-headers=1', httpbin_both + '/get')